Skip to content

Commit 2d81b11

Browse files
thomas-serre-sonarsourcesonartech
authored andcommitted
SONARPY-3656: Create rule S8392: FastAPI applications should not bind to all network interfaces (#782)
GitOrigin-RevId: f9bd468fa03d1459f01f81425e6c0ecc2746f83a
1 parent 7a3bb32 commit 2d81b11

13 files changed

Lines changed: 527 additions & 3 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.python.checks;
18+
19+
import org.sonar.check.Rule;
20+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
21+
import org.sonar.plugins.python.api.SubscriptionContext;
22+
import org.sonar.plugins.python.api.tree.CallExpression;
23+
import org.sonar.plugins.python.api.tree.RegularArgument;
24+
import org.sonar.plugins.python.api.tree.StringLiteral;
25+
import org.sonar.plugins.python.api.tree.Tree;
26+
import org.sonar.plugins.python.api.types.v2.matchers.TypeMatcher;
27+
import org.sonar.plugins.python.api.types.v2.matchers.TypeMatchers;
28+
import org.sonar.python.checks.utils.Expressions;
29+
import org.sonar.python.tree.TreeUtils;
30+
31+
@Rule(key = "S8392")
32+
public class FastAPIBindToAllNetworkInterfacesCheck extends PythonSubscriptionCheck {
33+
34+
private static final String ALL_NETWORK_INTERFACES = "0.0.0.0";
35+
private final static TypeMatcher UVICORN_RUN_FUNCTION_TYPE_MATCHER = TypeMatchers.isType("uvicorn.run");
36+
37+
@Override
38+
public void initialize(Context context) {
39+
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, this::checkUvicornRunFunctionCalls);
40+
}
41+
42+
private void checkUvicornRunFunctionCalls(SubscriptionContext ctx) {
43+
CallExpression callExpr = ((CallExpression) ctx.syntaxNode());
44+
45+
if (!UVICORN_RUN_FUNCTION_TYPE_MATCHER.isTrueFor(callExpr.callee(), ctx)) {
46+
return;
47+
}
48+
49+
RegularArgument hostArgument = TreeUtils.argumentByKeyword("host", callExpr.arguments());
50+
if (hostArgument == null) {
51+
return;
52+
}
53+
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+
}
57+
}
58+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ public Stream<Class<?>> getChecks() {
218218
ExecStatementUsageCheck.class,
219219
ExitHasBadArgumentsCheck.class,
220220
ExpandingArchiveCheck.class,
221+
FastAPIBindToAllNetworkInterfacesCheck.class,
221222
FastHashingOrPlainTextCheck.class,
222223
FieldDuplicatesClassNameCheck.class,
223224
FieldNameCheck.class,

python-checks/src/main/java/org/sonar/python/checks/utils/Expressions.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,22 @@ public static boolean isTruthy(@Nullable Expression expression) {
109109
return Expressions.isTruthyInternal(expression);
110110
}
111111

112+
@CheckForNull
113+
public static StringLiteral extractStringLiteral(Tree tree) {
114+
if (tree.is(Tree.Kind.STRING_LITERAL)) {
115+
return (StringLiteral) tree;
116+
}
117+
118+
if (tree.is(Tree.Kind.NAME)) {
119+
Expression assignedValue = Expressions.singleAssignedValue(((Name) tree));
120+
if (assignedValue != null && assignedValue.is(Tree.Kind.STRING_LITERAL)) {
121+
return ((StringLiteral) assignedValue);
122+
}
123+
}
124+
125+
return null;
126+
}
127+
112128
@CheckForNull
113129
public static Expression singleAssignedValue(Name name) {
114130
return singleAssignedValue(name, new HashSet<>());
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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>
3+
<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>
6+
<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
7+
application becomes accessible from:</p>
8+
<ul>
9+
<li> The local machine (localhost) </li>
10+
<li> The local network (LAN) </li>
11+
<li> Any public-facing network interfaces </li>
12+
<li> Virtual network interfaces </li>
13+
</ul>
14+
<p>This broad exposure violates the principle of least privilege, which states that a system should only have access to the resources it needs to
15+
function. By binding to all interfaces, you’re making your application accessible from networks where it shouldn’t be reachable.</p>
16+
<p>In development environments, this is particularly risky because:</p>
17+
<ul>
18+
<li> Development servers often lack production security measures </li>
19+
<li> Debug mode may be enabled, exposing sensitive information </li>
20+
<li> Authentication and authorization might not be fully implemented </li>
21+
<li> The application may contain test data or incomplete features </li>
22+
</ul>
23+
<p>Even in production environments, binding to <code>0.0.0.0</code> should be a deliberate choice made with proper security controls in place, such
24+
as:</p>
25+
<ul>
26+
<li> Firewalls restricting access to specific IP addresses </li>
27+
<li> Reverse proxies handling TLS/SSL termination </li>
28+
<li> Network segmentation isolating the application </li>
29+
<li> Container orchestration platforms managing network access </li>
30+
</ul>
31+
<p>When these controls are absent, binding to all interfaces creates an unnecessarily large attack surface.</p>
32+
<h3>What is the potential impact?</h3>
33+
<p>Binding to all network interfaces can lead to several security risks:</p>
34+
<ul>
35+
<li> <strong>Unauthorized access</strong>: Attackers on the same network or the internet can reach your application, potentially exploiting
36+
vulnerabilities or accessing sensitive data. </li>
37+
<li> <strong>Information disclosure</strong>: Debug endpoints, error messages, or development features may leak sensitive information about your
38+
application’s structure, dependencies, or data. </li>
39+
<li> <strong>Lateral movement</strong>: If an attacker compromises your network, an exposed development server can serve as an entry point to other
40+
systems. </li>
41+
<li> <strong>Data breaches</strong>: Unprotected applications may expose user data, API keys, database credentials, or other confidential
42+
information. </li>
43+
</ul>
44+
<p>The severity depends on the environment and what security controls are in place, but the risk is highest in development environments where security
45+
measures are typically minimal.</p>
46+
<h2>How to fix it</h2>
47+
<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
48+
application is only accessible from your local machine.</p>
49+
<h3>Code examples</h3>
50+
<h4>Noncompliant code example</h4>
51+
<pre data-diff-id="1" data-diff-type="noncompliant">
52+
import uvicorn
53+
from fastapi import FastAPI
54+
55+
app = FastAPI()
56+
57+
if __name__ == "__main__":
58+
uvicorn.run(app, host="0.0.0.0", port=8000) # Noncompliant
59+
</pre>
60+
<h4>Compliant solution</h4>
61+
<pre data-diff-id="1" data-diff-type="compliant">
62+
import uvicorn
63+
from fastapi import FastAPI
64+
65+
app = FastAPI()
66+
67+
if __name__ == "__main__":
68+
uvicorn.run(app, host="127.0.0.1", port=8000)
69+
</pre>
70+
<h2>Resources</h2>
71+
<h3>Documentation</h3>
72+
<ul>
73+
<li> FastAPI Documentation - Deployment Concepts - <a href="https://fastapi.tiangolo.com/deployment/concepts/">Official FastAPI documentation on
74+
deployment concepts, including network binding considerations</a> </li>
75+
<li> Uvicorn Documentation - Deployment - <a href="https://www.uvicorn.org/deployment/">Uvicorn server documentation on deployment and configuration
76+
options</a> </li>
77+
<li> FastAPI CLI Documentation - <a href="https://fastapi.tiangolo.com/fastapi-cli/">Documentation explaining the difference between development
78+
(127.0.0.1) and production (0.0.0.0) binding</a> </li>
79+
</ul>
80+
<h3>Standards</h3>
81+
<ul>
82+
<li> CWE 668 - <a href="https://cwe.mitre.org/data/definitions/668.html">Exposure of Resource to Wrong Sphere</a> </li>
83+
<li> CIS 6.6.1 - <a href="https://www.cisecurity.org/controls/">CIS Controls - Network Security</a> </li>
84+
</ul>
85+
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"title": "FastAPI applications should not bind to all network interfaces",
3+
"type": "VULNERABILITY",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "5min"
8+
},
9+
"tags": [
10+
"fastapi",
11+
"uvicorn",
12+
"network",
13+
"deployment",
14+
"least-privilege"
15+
],
16+
"defaultSeverity": "Blocker",
17+
"ruleSpecification": "RSPEC-8392",
18+
"sqKey": "S8392",
19+
"scope": "Main",
20+
"quickfix": "unknown",
21+
"code": {
22+
"impacts": {
23+
"SECURITY": "BLOCKER"
24+
},
25+
"attribute": "CONVENTIONAL"
26+
},
27+
"securityStandards": {
28+
"CWE": [
29+
668
30+
],
31+
"CIS": [
32+
"6.6.1"
33+
]
34+
}
35+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@
308308
"S7943",
309309
"S7945",
310310
"S8375",
311+
"S8392",
311312
"S8396"
312313
]
313314
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.python.checks;
18+
19+
import org.junit.jupiter.api.Test;
20+
import org.sonar.python.checks.utils.PythonCheckVerifier;
21+
22+
class FastAPIBindToAllNetworkInterfacesCheckTest {
23+
24+
@Test
25+
void test() {
26+
PythonCheckVerifier.verify("src/test/resources/checks/fastAPIBindToAllNetworkInterfaces.py", new FastAPIBindToAllNetworkInterfacesCheck());
27+
}
28+
29+
}

python-checks/src/test/java/org/sonar/python/checks/utils/ExpressionsTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,36 @@ void singleAssignedNonNameValue() {
292292
assertThat(lastNameNonNameValue("x = 42; y")).isNull();
293293
}
294294

295+
@Test
296+
void extract_string_literal_from_direct_literal() {
297+
StringLiteral direct = Expressions.extractStringLiteral(exp("'aStringLiteral'"));
298+
assertThat(direct).isNotNull();
299+
assertThat(direct.trimmedQuotesValue()).isEqualTo("aStringLiteral");
300+
}
301+
302+
@Test
303+
void extract_string_literal_from_assigned_literal() {
304+
// name assigned to a string literal
305+
FileInput root = parse("x = 'aStringLiteralAssignedToAVariable'; x");
306+
new SymbolTableBuilder(null).visitFileInput(root);
307+
NameVisitor nameVisitor = new NameVisitor();
308+
root.accept(nameVisitor);
309+
Name lastName = nameVisitor.names.get(nameVisitor.names.size() - 1);
310+
StringLiteral assigned = Expressions.extractStringLiteral(lastName);
311+
assertThat(assigned).isNotNull();
312+
assertThat(assigned.trimmedQuotesValue()).isEqualTo("aStringLiteralAssignedToAVariable");
313+
}
314+
315+
@Test
316+
void extract_string_literal_from_non_literal_return_null() {
317+
FileInput root = parse("y = 42; y");
318+
new SymbolTableBuilder(null).visitFileInput(root);
319+
NameVisitor nameVisitor = new NameVisitor();
320+
root.accept(nameVisitor);
321+
Name lastY = nameVisitor.names.get(nameVisitor.names.size() - 1);
322+
assertThat(Expressions.extractStringLiteral(lastY)).isNull();
323+
}
324+
295325
private Expression lastNameNonNameValue(String code) {
296326
FileInput root = parse(code);
297327
new SymbolTableBuilder(null).visitFileInput(root);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import uvicorn
2+
from fastapi import FastAPI
3+
from somewhere import my_host_name
4+
5+
app = FastAPI()
6+
7+
uvicorn.run(app, host="127.0.0.1")
8+
uvicorn.run(app, port=8000)
9+
uvicorn.run(app, host="127.0.0.1", port=8000)
10+
uvicorn.run(app, host="localhost", port=8000)
11+
uvicorn.run(app, host="192.168.1.100", port=8000)
12+
uvicorn.run(app, host=my_host_name, port=8000)
13+
14+
uvicorn.run(app, host="0.0.0.0") # Noncompliant {{Avoid binding the FastAPI application to all network interfaces.}}
15+
uvicorn.run(app, host="0.0.0.0", port=8000) # Noncompliant
16+
uvicorn.run(app, host="0.0.0.0", port=8000, debug=True) # Noncompliant
17+
host_config = "0.0.0.0"
18+
uvicorn.run(app, host=host_config, port=8000) # Noncompliant

0 commit comments

Comments
 (0)