Skip to content

Commit 046031d

Browse files
guillaume-dequennesonartech
authored andcommitted
GitOrigin-RevId: 885873307daec29225cd3acbce469ed8fc4786ba
1 parent 33b2be9 commit 046031d

2 files changed

Lines changed: 133 additions & 0 deletions

File tree

python-checks/src/main/java/org/sonar/python/checks/hotspots/UnverifiedHostnameCheck.java

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919
import java.util.Arrays;
2020
import java.util.Collections;
2121
import java.util.HashSet;
22+
import java.util.Objects;
23+
import java.util.Optional;
2224
import java.util.Set;
25+
import java.util.function.Predicate;
2326
import org.sonar.check.Rule;
2427
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
2528
import org.sonar.plugins.python.api.SubscriptionContext;
@@ -29,12 +32,15 @@
2932
import org.sonar.plugins.python.api.tree.CallExpression;
3033
import org.sonar.plugins.python.api.tree.Expression;
3134
import org.sonar.plugins.python.api.tree.HasSymbol;
35+
import org.sonar.plugins.python.api.tree.Name;
3236
import org.sonar.plugins.python.api.tree.QualifiedExpression;
3337
import org.sonar.plugins.python.api.tree.RegularArgument;
3438
import org.sonar.plugins.python.api.tree.Tree;
3539
import org.sonar.plugins.python.api.types.v2.PythonType;
3640
import org.sonar.plugins.python.api.types.v2.TriBool;
3741
import org.sonar.python.checks.utils.Expressions;
42+
import org.sonar.python.semantic.v2.SymbolV2;
43+
import org.sonar.python.semantic.v2.UsageV2;
3844
import org.sonar.python.tree.TreeUtils;
3945
import org.sonar.python.types.v2.TypeCheckBuilder;
4046

@@ -43,14 +49,25 @@ public class UnverifiedHostnameCheck extends PythonSubscriptionCheck {
4349

4450
private static final String MESSAGE = "Enable server hostname verification on this SSL/TLS connection.";
4551
private static final String SECONDARY_OPENSSL = "This context does not perform hostname verification.";
52+
private static final String SECONDARY_DISABLED = "Hostname verification is disabled here.";
4653

4754
private static final Set<String> SECURE_BY_DEFAULT = new HashSet<>(Arrays.asList("ssl.create_default_context", "ssl._create_default_https_context"));
4855
private static final Set<String> UNSECURE_BY_DEFAULT = new HashSet<>(Arrays.asList("ssl._create_unverified_context", "ssl._create_stdlib_context"));
4956

57+
private static final Set<String> UNSAFE_PROTOCOLS = Set.of(
58+
"ssl.PROTOCOL_SSLv23",
59+
"ssl.PROTOCOL_TLS",
60+
"ssl.PROTOCOL_SSLv3",
61+
"ssl.PROTOCOL_TLSv1",
62+
"ssl.PROTOCOL_TLSv1_1",
63+
"ssl.PROTOCOL_TLSv1_2"
64+
);
65+
5066
private static Set<String> functionsToCheck;
5167

5268
private TypeCheckBuilder openSSLConnectionTypeCheckBuilder;
5369
private TypeCheckBuilder openSSLContextTypeCheckBuilder;
70+
private TypeCheckBuilder sslContextTypeCheckBuilder;
5471

5572
private static Set<String> functionsToCheck() {
5673
if (functionsToCheck == null) {
@@ -109,6 +126,7 @@ public void initialize(Context context) {
109126
context.registerSyntaxNodeConsumer(Tree.Kind.FILE_INPUT, ctx -> {
110127
openSSLConnectionTypeCheckBuilder = ctx.typeChecker().typeCheckBuilder().isTypeWithName("OpenSSL.SSL.Connection");
111128
openSSLContextTypeCheckBuilder = ctx.typeChecker().typeCheckBuilder().isInstanceOf("OpenSSL.SSL.Context");
129+
sslContextTypeCheckBuilder = ctx.typeChecker().typeCheckBuilder().isTypeWithName("ssl.SSLContext");
112130
});
113131
}
114132

@@ -119,6 +137,83 @@ private void checkCallExpression(SubscriptionContext ctx) {
119137
checkSuspiciousCall(callExpression, calleeSymbol, ctx);
120138
}
121139
checkOpenSSLConnection(ctx, callExpression);
140+
checkSSLContext(ctx, callExpression);
141+
}
142+
143+
private void checkSSLContext(SubscriptionContext ctx, CallExpression callExpression) {
144+
Expression callee = callExpression.callee();
145+
PythonType pythonType = callee.typeV2();
146+
147+
if (sslContextTypeCheckBuilder.check(pythonType) != TriBool.TRUE) {
148+
return;
149+
}
150+
151+
Optional<Symbol> protocolSymbolOpt = getProtocolSymbol(callExpression);
152+
if (protocolSymbolOpt.isEmpty()) {
153+
return;
154+
}
155+
156+
Symbol protocolSymbol = protocolSymbolOpt.get();
157+
String protocolFQN = protocolSymbol.fullyQualifiedName();
158+
if (protocolFQN == null) {
159+
return;
160+
}
161+
162+
Tree parent = TreeUtils.firstAncestorOfKind(callExpression, Tree.Kind.ASSIGNMENT_STMT);
163+
if (parent == null) {
164+
// Direct usage case where context is created and used without assignment
165+
if (UNSAFE_PROTOCOLS.contains(protocolFQN)) {
166+
ctx.addIssue(callExpression, MESSAGE);
167+
}
168+
return;
169+
}
170+
171+
Optional<SymbolV2> contextSymbol = getContextSymbol((AssignmentStatement) parent);
172+
if (contextSymbol.isEmpty()) {
173+
return;
174+
}
175+
176+
if (UNSAFE_PROTOCOLS.contains(protocolFQN)) {
177+
// For unsafe protocols, check_hostname should explicitly be set to true
178+
Optional<AssignmentStatement> hostnameEnabledAssignment = findCheckHostnameStatement(contextSymbol.get(), Expressions::isTruthy);
179+
if (hostnameEnabledAssignment.isEmpty()) {
180+
ctx.addIssue(callExpression, MESSAGE);
181+
}
182+
} else {
183+
// For safe protocols, report if check_hostname is explicitly set to false
184+
Optional<AssignmentStatement> hostnameDisabledAssignment = findCheckHostnameStatement(contextSymbol.get(), Expressions::isFalsy);
185+
hostnameDisabledAssignment.ifPresent(assignment -> {
186+
PreciseIssue issue = ctx.addIssue(callee, MESSAGE);
187+
issue.secondary(assignment, SECONDARY_DISABLED);
188+
});
189+
}
190+
}
191+
192+
private static Optional<Symbol> getProtocolSymbol(CallExpression callExpression) {
193+
return Optional.ofNullable(TreeUtils.nthArgumentOrKeyword(0, "protocol", callExpression.arguments()))
194+
.map(RegularArgument::expression)
195+
.filter(HasSymbol.class::isInstance)
196+
.map(expr -> ((HasSymbol) expr).symbol());
197+
}
198+
199+
private static Optional<SymbolV2> getContextSymbol(AssignmentStatement assignmentStatement) {
200+
return Optional.ofNullable(assignmentStatement.lhsExpressions().get(0).expressions().get(0))
201+
.filter(Name.class::isInstance)
202+
.map(expr -> ((Name) expr).symbolV2());
203+
}
204+
205+
private static Optional<AssignmentStatement> findCheckHostnameStatement(SymbolV2 contextSymbol, Predicate<Expression> valueCheck) {
206+
return contextSymbol.usages().stream()
207+
.map(UsageV2::tree)
208+
.map(Tree::parent)
209+
.filter(QualifiedExpression.class::isInstance)
210+
.map(QualifiedExpression.class::cast)
211+
.filter(qe -> "check_hostname".equals(qe.name().name()))
212+
.map(t -> TreeUtils.firstAncestorOfKind(t, Tree.Kind.ASSIGNMENT_STMT))
213+
.filter(Objects::nonNull)
214+
.map(AssignmentStatement.class::cast)
215+
.filter(a -> valueCheck.test(a.assignedValue()))
216+
.findFirst();
122217
}
123218

124219
private void checkOpenSSLConnection(SubscriptionContext ctx, CallExpression callExpression) {
@@ -145,3 +240,4 @@ private void checkOpenSSLConnection(SubscriptionContext ctx, CallExpression call
145240
}
146241
}
147242
}
243+

python-checks/src/test/resources/checks/hotspots/unverifiedHostname.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,43 @@
4747
if ssl._create_unverified_context() == whatever:
4848
pass
4949

50+
def ssl_context_constructor():
51+
ctx1 = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) # Noncompliant
52+
ctx2 = ssl.SSLContext(ssl.PROTOCOL_TLS) # Noncompliant
53+
ctx3 = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) # Compliant
54+
ctx4 = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) # Compliant
55+
56+
foo(ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)) # Noncompliant
57+
foo(ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)) # Compliant
58+
59+
ctx5 = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
60+
ctx5.check_hostname = True # Compliant
61+
62+
ctx6 = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) # Noncompliant
63+
ctx6.check_hostname = False
64+
65+
ctx7 = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) # Noncompliant {{Enable server hostname verification on this SSL/TLS connection.}}
66+
# ^^^^^^^^^^^^^^
67+
ctx7.check_hostname = False
68+
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^< {{Hostname verification is disabled here.}}
69+
70+
71+
def edge_cases():
72+
nonlocal no_symbol
73+
ssl.SSLContext(no_symbol)
74+
75+
if cond:
76+
from foo import bar
77+
else:
78+
bar = 42
79+
ssl.SSLContext(bar)
80+
81+
something[1] = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) # FN
82+
83+
ssl.SSLContext()
84+
ssl.SSLContext(abc[42])
85+
86+
5087
def pyopenssl_noncompliant():
5188
import socket
5289
from OpenSSL import SSL

0 commit comments

Comments
 (0)