Skip to content

Commit b34a7c9

Browse files
joke1196thomas-serre-sonarsource
authored andcommitted
SONARPY-3640: Update S8392 to unify with Flask applications (#787)
Co-authored-by: Thomas Serre <thomas.serre@sonarsource.com> Co-authored-by: Thomas Serre <118730793+thomas-serre-sonarsource@users.noreply.github.com> GitOrigin-RevId: 66f75d4a18b185ac555442d8a1a422e3b6eb770c
1 parent ccd8185 commit b34a7c9

7 files changed

Lines changed: 118 additions & 41 deletions

File tree

python-checks/src/main/java/org/sonar/python/checks/FastAPIBindToAllNetworkInterfacesCheck.java renamed to python-checks/src/main/java/org/sonar/python/checks/BindToAllNetworkInterfacesCheck.java

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
package org.sonar.python.checks;
1818

19+
import javax.annotation.Nullable;
1920
import org.sonar.check.Rule;
2021
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
2122
import org.sonar.plugins.python.api.SubscriptionContext;
@@ -29,30 +30,36 @@
2930
import org.sonar.python.tree.TreeUtils;
3031

3132
@Rule(key = "S8392")
32-
public class FastAPIBindToAllNetworkInterfacesCheck extends PythonSubscriptionCheck {
33+
public class BindToAllNetworkInterfacesCheck extends PythonSubscriptionCheck {
3334

3435
private static final String ALL_NETWORK_INTERFACES = "0.0.0.0";
35-
private static final TypeMatcher UVICORN_RUN_FUNCTION_TYPE_MATCHER = TypeMatchers.isType("uvicorn.run");
36+
private static final String MESSAGE = "Avoid binding the application to all network interfaces.";
37+
private static final TypeMatcher UVICORN_APP_RUN_TYPE_MATCHER = TypeMatchers.isType("uvicorn.run");
38+
private static final TypeMatcher FLASK_APP_RUN_TYPE_MATCHER = TypeMatchers.isType("flask.app.Flask.run");
3639

3740
@Override
3841
public void initialize(Context context) {
39-
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, FastAPIBindToAllNetworkInterfacesCheck::checkUvicornRunFunctionCalls);
42+
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, BindToAllNetworkInterfacesCheck::checkFunctionCalls);
4043
}
41-
42-
private static void checkUvicornRunFunctionCalls(SubscriptionContext ctx) {
44+
private static void checkFunctionCalls(SubscriptionContext ctx) {
4345
CallExpression callExpr = ((CallExpression) ctx.syntaxNode());
44-
45-
if (!UVICORN_RUN_FUNCTION_TYPE_MATCHER.isTrueFor(callExpr.callee(), ctx)) {
46-
return;
46+
RegularArgument hostArgument = null;
47+
if (UVICORN_APP_RUN_TYPE_MATCHER.isTrueFor(callExpr.callee(), ctx)) {
48+
hostArgument = TreeUtils.argumentByKeyword("host", callExpr.arguments());
49+
} else if (FLASK_APP_RUN_TYPE_MATCHER.isTrueFor(callExpr.callee(), ctx)) {
50+
hostArgument = TreeUtils.nthArgumentOrKeyword(0, "host", callExpr.arguments());
51+
}
52+
if (isHostBoundToAll(hostArgument)) {
53+
ctx.addIssue(callExpr.callee(), MESSAGE);
4754
}
55+
}
4856

49-
RegularArgument hostArgument = TreeUtils.argumentByKeyword("host", callExpr.arguments());
50-
if (hostArgument == null) {
51-
return;
57+
private static boolean isHostBoundToAll(@Nullable RegularArgument hostArgument) {
58+
if (hostArgument==null){
59+
return false;
5260
}
5361
StringLiteral hostValue = Expressions.extractStringLiteral(hostArgument.expression());
54-
if (hostValue != null && ALL_NETWORK_INTERFACES.equals(hostValue.trimmedQuotesValue())) {
55-
ctx.addIssue(hostArgument, "Avoid binding the FastAPI application to all network interfaces.");
56-
}
62+
return hostValue != null && ALL_NETWORK_INTERFACES.equals(hostValue.trimmedQuotesValue());
5763
}
5864
}
65+

python-checks/src/main/java/org/sonar/python/checks/OpenSourceCheckList.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ public Stream<Class<?>> getChecks() {
143143
BackslashInStringCheck.class,
144144
BackticksUsageCheck.class,
145145
BareRaiseInFinallyCheck.class,
146+
BindToAllNetworkInterfacesCheck.class,
146147
BooleanExpressionInExceptCheck.class,
147148
BooleanCheckNotInvertedCheck.class,
148149
BreakContinueOutsideLoopCheck.class,
@@ -218,7 +219,6 @@ public Stream<Class<?>> getChecks() {
218219
ExecStatementUsageCheck.class,
219220
ExitHasBadArgumentsCheck.class,
220221
ExpandingArchiveCheck.class,
221-
FastAPIBindToAllNetworkInterfacesCheck.class,
222222
FastAPIDependencyAnnotatedCheck.class,
223223
FastAPIFileUploadFormCheck.class,
224224
FastAPIGenericRouteDecoratorCheck.class,

python-checks/src/main/resources/org/sonar/l10n/py/rules/python/S8392.html

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
<p>This is an issue when a FastAPI application uses <code>uvicorn.run()</code> with <code>host="0.0.0.0"</code>, which binds the application to all
2-
available network interfaces on the host machine.</p>
1+
<p>This is an issue when a web server uses <code>host="0.0.0.0"</code>, which binds the application to all available network interfaces on the host
2+
machine.</p>
33
<h2>Why is this an issue?</h2>
4-
<p>When you start a FastAPI application, you need to specify which network interface it should listen on. A network interface is a connection point
5-
between your computer and a network.</p>
4+
<p>When you start a Web server, you need to specify which network interface it should listen on. A network interface is a connection point between
5+
your computer and a network.</p>
66
<p>The special IP address <code>0.0.0.0</code> tells the application to bind to <strong>all</strong> network interfaces on the machine. This means the
77
application becomes accessible from:</p>
88
<ul>
@@ -43,7 +43,46 @@ <h3>What is the potential impact?</h3>
4343
</ul>
4444
<p>The severity depends on the environment and what security controls are in place, but the risk is highest in development environments where security
4545
measures are typically minimal.</p>
46-
<h2>How to fix it</h2>
46+
<h2>How to fix it in Flask</h2>
47+
<p>For development, bind to localhost only. For production, use a proper WSGI server instead of app.run().</p>
48+
<h3>Code examples</h3>
49+
<h4>Noncompliant code example</h4>
50+
<pre data-diff-id="1" data-diff-type="noncompliant">
51+
from flask import Flask
52+
app = Flask(__name__)
53+
@app.route('/')
54+
def hello():
55+
return 'Hello World!'
56+
if __name__ == '__main__':
57+
app.run(host='0.0.0.0', debug=True) # Noncompliant
58+
</pre>
59+
<h4>Compliant solution</h4>
60+
<pre data-diff-id="1" data-diff-type="compliant">
61+
from flask import Flask
62+
app = Flask(__name__)
63+
@app.route('/')
64+
def hello():
65+
return 'Hello World!'
66+
if __name__ == '__main__':
67+
app.run(host='0.0.0.0', debug=False)
68+
</pre>
69+
<h4>Noncompliant code example</h4>
70+
<pre data-diff-id="2" data-diff-type="noncompliant">
71+
import os
72+
from flask import Flask
73+
app = Flask(__name__)
74+
if __name__ == '__main__':
75+
app.run(host='0.0.0.0', port=5000, debug=True) # Noncompliant
76+
</pre>
77+
<h4>Compliant solution</h4>
78+
<pre data-diff-id="2" data-diff-type="compliant">
79+
import os
80+
from flask import Flask
81+
app = Flask(__name__)
82+
if __name__ == '__main__':
83+
app.run(host='127.0.0.1', port=5000, debug=True)
84+
</pre>
85+
<h2>How to fix it in FastAPI</h2>
4786
<p>For development and local testing, bind to localhost (<code>127.0.0.1</code> or <code>localhost</code>) instead of all interfaces. This ensures the
4887
application is only accessible from your local machine.</p>
4988
<h3>Code examples</h3>
@@ -76,6 +115,11 @@ <h3>Documentation</h3>
76115
options</a> </li>
77116
<li> FastAPI CLI Documentation - <a href="https://fastapi.tiangolo.com/fastapi-cli/">Documentation explaining the difference between development
78117
(127.0.0.1) and production (0.0.0.0) binding</a> </li>
118+
<li> Flask Deployment Options - <a href="https://flask.palletsprojects.com/en/2.3.x/deploying/">Official Flask documentation on secure deployment
119+
practices</a> </li>
120+
<li> Flask Security Considerations - <a href="https://flask.palletsprojects.com/en/2.3.x/security/">Flask security best practices and common
121+
vulnerabilities</a> </li>
122+
<li> Gunicorn Documentation - <a href="https://docs.gunicorn.org/en/stable/">Production WSGI server for Python web applications</a> </li>
79123
</ul>
80124
<h3>Standards</h3>
81125
<ul>

python-checks/src/main/resources/org/sonar/l10n/py/rules/python/S8392.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"title": "FastAPI applications should not bind to all network interfaces",
2+
"title": "Web servers should not bind to all network interfaces",
33
"type": "VULNERABILITY",
44
"status": "ready",
55
"remediation": {

python-checks/src/test/java/org/sonar/python/checks/FastAPIBindToAllNetworkInterfacesCheckTest.java renamed to python-checks/src/test/java/org/sonar/python/checks/BindToAllNetworkInterfacesCheckTest.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@
1919
import org.junit.jupiter.api.Test;
2020
import org.sonar.python.checks.utils.PythonCheckVerifier;
2121

22-
class FastAPIBindToAllNetworkInterfacesCheckTest {
22+
class BindToAllNetworkInterfacesCheckTest {
2323

2424
@Test
2525
void test() {
26-
PythonCheckVerifier.verify("src/test/resources/checks/fastAPIBindToAllNetworkInterfaces.py", new FastAPIBindToAllNetworkInterfacesCheck());
26+
PythonCheckVerifier.verify("src/test/resources/checks/bindToAllNetworkInterfaces.py", new BindToAllNetworkInterfacesCheck());
2727
}
2828

2929
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import uvicorn
2+
from fastapi import FastAPI
3+
from flask import Flask
4+
from somewhere import my_host_name
5+
6+
def fast_api_tests():
7+
app_fastapi = FastAPI()
8+
9+
uvicorn.run(app_fastapi, host="127.0.0.1")
10+
uvicorn.run(app_fastapi, port=8000)
11+
uvicorn.run(app_fastapi, host="127.0.0.1", port=8000)
12+
uvicorn.run(app_fastapi, host="localhost", port=8000)
13+
uvicorn.run(app_fastapi, host="192.168.1.100", port=8000)
14+
uvicorn.run(app_fastapi, host=my_host_name, port=8000)
15+
16+
uvicorn.run(app_fastapi, host="0.0.0.0") # Noncompliant {{Avoid binding the application to all network interfaces.}}
17+
# ^^^^^^^^^^^
18+
19+
uvicorn.run(app_fastapi, host="0.0.0.0", port=8000) # Noncompliant
20+
uvicorn.run(app_fastapi, host="0.0.0.0", port=8000, debug=True) # Noncompliant
21+
host_config = "0.0.0.0"
22+
uvicorn.run(app_fastapi, host=host_config, port=8000) # Noncompliant
23+
24+
def flask_tests():
25+
app_flask = Flask(__name__)
26+
27+
app_flask.run(host='0.0.0.0', debug=True) # Noncompliant {{Avoid binding the application to all network interfaces.}}
28+
# ^^^^^^^^^^^^^
29+
app_flask.run('0.0.0.0', 5000, True) # Noncompliant
30+
app_flask.run(host='0.0.0.0', debug=False) # Noncompliant
31+
app_flask.run(host='0.0.0.0') # Noncompliant
32+
app_flask.run(host='127.0.0.1', debug=True)
33+
app_flask.run(host='', debug=True)
34+
app_flask.run('', debug=True)
35+
app_flask.run(None, debug=True)
36+
debug = True
37+
host = '0.0.0.0'
38+
app_flask.run(host=host, debug=debug) # Noncompliant
39+
other_host = '12'
40+
app_flask.run(other_host, debug=debug)
41+
app_flask.run() # Incorrect syntax
42+
reassigned = '0.0.0.0'
43+
app_flask.run(reassigned, debug=True) # FN limitation of singleAssignedValue
44+
reassigned = '0.0.0.0'

python-checks/src/test/resources/checks/fastAPIBindToAllNetworkInterfaces.py

Lines changed: 0 additions & 18 deletions
This file was deleted.

0 commit comments

Comments
 (0)