Skip to content

Commit 12d3e89

Browse files
maksim-grebeniuk-sonarsourcesonartech
authored andcommitted
SONARPY-1698 [S4830] Improve ssl support (#198)
GitOrigin-RevId: 17769ce4bd2748ceb796c6e2544ee21755126258
1 parent 2339e88 commit 12d3e89

2 files changed

Lines changed: 114 additions & 40 deletions

File tree

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

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

19-
import java.util.ArrayList;
2019
import java.util.Arrays;
20+
import java.util.Collection;
2121
import java.util.Collections;
2222
import java.util.Comparator;
2323
import java.util.HashSet;
@@ -38,6 +38,7 @@
3838
import org.sonar.plugins.python.api.tree.CallExpression;
3939
import org.sonar.plugins.python.api.tree.DictionaryLiteral;
4040
import org.sonar.plugins.python.api.tree.Expression;
41+
import org.sonar.plugins.python.api.tree.ExpressionList;
4142
import org.sonar.plugins.python.api.tree.HasSymbol;
4243
import org.sonar.plugins.python.api.tree.KeyValuePair;
4344
import org.sonar.plugins.python.api.tree.Name;
@@ -393,28 +394,36 @@ private static boolean isFalsyRange(CallExpression callExpr, String fqn) {
393394

394395
private static void standardSslCheckForAssignmentStatement(SubscriptionContext subscriptionContext) {
395396
AssignmentStatement asgnStmt = (AssignmentStatement) subscriptionContext.syntaxNode();
397+
TreeUtils.toOptionalInstanceOf(CallExpression.class, asgnStmt.assignedValue())
398+
.flatMap(VerifiedSslTlsCertificateCheck::isVulnerableMethodCall)
399+
.ifPresent(vulnTok -> checkVulnerableMethodCallAssignment(subscriptionContext, asgnStmt, vulnTok));
400+
}
396401

397-
Optional<VulnerabilityAndProblematicToken> vulnTokOpt = isVulnerableMethodCall(asgnStmt.assignedValue());
398-
vulnTokOpt.ifPresent(vulnTok -> asgnStmt
399-
.lhsExpressions()
402+
private static void checkVulnerableMethodCallAssignment(SubscriptionContext subscriptionContext, AssignmentStatement asgnStmt, VulnerabilityAndProblematicToken vulnTok) {
403+
asgnStmt.lhsExpressions()
400404
.stream()
401-
.flatMap(it -> it.expressions().stream())
405+
.map(ExpressionList::expressions)
406+
.flatMap(Collection::stream)
402407
.findFirst()
403-
.filter(Name.class::isInstance)
404-
.map(expr -> ((Name) expr).symbol())
405-
.ifPresent(symb -> {
406-
for (Usage u : selectRelevantModifyingUsages(symb.usages(), vulnTok.token.line())) {
408+
.flatMap(TreeUtils::getSymbolFromTree)
409+
.map(Symbol::usages)
410+
.map(usages -> selectRelevantModifyingUsages(usages, vulnTok.token.line()))
411+
.filter(usages -> usages.stream().noneMatch(VerifiedSslTlsCertificateCheck::isCheckHostnameAttributeSetToTrue))
412+
.ifPresent(usages -> {
413+
for (Usage u : usages) {
407414
searchForVerifyModeOverride(u).ifPresent(vulnTok::overrideBy);
408415
}
409416
if (vulnTok.isVulnerable) {
410417
subscriptionContext.addIssue(vulnTok.token, MESSAGE);
411418
}
412-
}));
419+
});
413420
}
414421

415422
private static void standardSslCheckForRegularArgument(SubscriptionContext subscriptionContext) {
416-
var argument = (RegularArgument) subscriptionContext.syntaxNode();
417-
isVulnerableMethodCall(argument.expression())
423+
TreeUtils.toOptionalInstanceOf(RegularArgument.class, subscriptionContext.syntaxNode())
424+
.map(RegularArgument::expression)
425+
.flatMap(TreeUtils.toOptionalInstanceOfMapper(CallExpression.class))
426+
.flatMap(VerifiedSslTlsCertificateCheck::isVulnerableMethodCall)
418427
.ifPresent(vulnTok -> subscriptionContext.addIssue(vulnTok.token, MESSAGE));
419428
}
420429

@@ -442,24 +451,45 @@ private static int findNextAssignmentLine(List<Usage> usages, int firstAssignmen
442451
* should suffice here.
443452
*/
444453
private static List<Usage> selectRelevantModifyingUsages(List<Usage> usages, int firstAssignmentLine) {
445-
int nextAssignmentLine = findNextAssignmentLine(usages, firstAssignmentLine);
446-
ArrayList<Usage> result = new ArrayList<Usage>();
447-
usages.stream().filter(u -> {
448-
int line = u.tree().firstToken().line();
449-
return !u.isBindingUsage() && line > firstAssignmentLine && line < nextAssignmentLine;
450-
}).forEach(u -> result.add(u));
451-
result.sort(Comparator.comparing(u -> u.tree().firstToken().line()));
452-
return result;
454+
var nextAssignmentLine = findNextAssignmentLine(usages, firstAssignmentLine);
455+
return usages.stream()
456+
.filter(Predicate.not(Usage::isBindingUsage))
457+
.filter(u -> {
458+
var line = u.tree().firstToken().line();
459+
return line > firstAssignmentLine && line < nextAssignmentLine;
460+
})
461+
.sorted(Comparator.comparing(u -> u.tree().firstToken().line()))
462+
.toList();
453463
}
454464

455465
/**
456466
* Map from FQNs of sensitive context factories to the boolean that determines whether default settings are dangerous.
457467
*/
458-
private static final Map<String, Boolean> VULNERABLE_CONTEXT_FACTORIES = Map.of(
459-
"ssl._create_unverified_context", true,
460-
"ssl._create_stdlib_context", true,
461-
"ssl.create_default_context", false,
462-
"ssl._create_default_https_context", false);
468+
private static final Map<String, Predicate<CallExpression>> VULNERABLE_CONTEXT_FACTORIES = Map.of(
469+
"ssl._create_unverified_context", c -> true,
470+
"ssl._create_stdlib_context", c -> true,
471+
"ssl.create_default_context", c -> false,
472+
"ssl._create_default_https_context", c -> false,
473+
"ssl.SSLContext", VerifiedSslTlsCertificateCheck::isVulnerableSslContextInstantiation
474+
);
475+
476+
private static final Set<String> VULNERABLE_SSL_PROTOCOLS = Set.of(
477+
"ssl.PROTOCOL_SSLv23",
478+
"ssl.PROTOCOL_TLS",
479+
"ssl.PROTOCOL_SSLv3",
480+
"ssl.PROTOCOL_TLSv1",
481+
"ssl.PROTOCOL_TLSv1_1",
482+
"ssl.PROTOCOL_TLSv1_2"
483+
);
484+
485+
private static boolean isVulnerableSslContextInstantiation(CallExpression sslConstructorCallExpression) {
486+
return TreeUtils.nthArgumentOrKeywordOptional(0, "protocol", sslConstructorCallExpression.arguments())
487+
.map(RegularArgument::expression)
488+
.flatMap(TreeUtils::getSymbolFromTree)
489+
.map(Symbol::fullyQualifiedName)
490+
.filter(VULNERABLE_SSL_PROTOCOLS::contains)
491+
.isPresent();
492+
}
463493

464494
/**
465495
* Pair and a mutable cell for combining all updates to <code>verify_mode</code>.
@@ -490,21 +520,15 @@ void overrideBy(VulnerabilityAndProblematicToken overridingAssignment) {
490520
* if found, returns the token of the callee, together with the boolean that indicates whether the default settings
491521
* are dangerous.
492522
*/
493-
private static Optional<VulnerabilityAndProblematicToken> isVulnerableMethodCall(Expression expr) {
494-
if (expr instanceof CallExpression callExpression) {
495-
Symbol calleeSymbol = callExpression.calleeSymbol();
496-
if (calleeSymbol != null) {
497-
String fqn = calleeSymbol.fullyQualifiedName();
498-
if (fqn != null && VULNERABLE_CONTEXT_FACTORIES.containsKey(fqn)) {
499-
boolean isVulnerable = VULNERABLE_CONTEXT_FACTORIES.get(fqn);
500-
return Optional.of(new VulnerabilityAndProblematicToken(
501-
isVulnerable,
502-
callExpression.callee().lastToken(),
503-
true));
504-
}
505-
}
506-
}
507-
return Optional.empty();
523+
private static Optional<VulnerabilityAndProblematicToken> isVulnerableMethodCall(CallExpression callExpression) {
524+
return Optional.ofNullable(callExpression.calleeSymbol())
525+
.map(Symbol::fullyQualifiedName)
526+
.map(VULNERABLE_CONTEXT_FACTORIES::get)
527+
.map(predicate -> predicate.test(callExpression))
528+
.map(isVulnerable -> new VulnerabilityAndProblematicToken(
529+
isVulnerable,
530+
callExpression.callee().lastToken(),
531+
true));
508532
}
509533

510534
private static Optional<VulnerabilityAndProblematicToken> searchForVerifyModeOverride(Usage u) {
@@ -529,4 +553,19 @@ private static Optional<VulnerabilityAndProblematicToken> searchForVerifyModeOve
529553
}
530554
return Optional.empty();
531555
}
556+
557+
private static boolean isCheckHostnameAttributeSetToTrue(Usage u) {
558+
return Optional.of(u)
559+
.filter(Predicate.not(Usage::isBindingUsage))
560+
.map(Usage::tree)
561+
.map(Tree::parent)
562+
.flatMap(TreeUtils.toOptionalInstanceOfMapper(QualifiedExpression.class))
563+
.filter(qe -> "check_hostname".equals(qe.name().name()))
564+
.map(QualifiedExpression::parent)
565+
.map(Tree::parent)
566+
.flatMap(TreeUtils.toOptionalInstanceOfMapper(AssignmentStatement.class))
567+
.map(AssignmentStatement::assignedValue)
568+
.filter(Expressions::isTruthy)
569+
.isPresent();
570+
}
532571
}

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,41 @@ def call_expression_in_arguments():
255255
conn = httplib.client.HTTPSConnection("123.123.21.21", context=ssl._create_unverified_context()) # Noncompliant {{Enable server certificate validation on this SSL/TLS connection.}}
256256
# ^^^^^^^^^^^^^^^^^^^^^^^^^^ 0
257257

258+
def sslSetVerifyModeRequired():
259+
import ssl
260+
261+
ctx1 = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) # Noncompliant
262+
ctx2 = ssl.SSLContext(ssl.PROTOCOL_TLS) # Noncompliant
263+
ctx3 = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) # Compliant
264+
ctx4 = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) # Compliant
265+
266+
ctx5 = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
267+
ctx5.verify_mode = ssl.CERT_REQUIRED # Compliant
268+
269+
ctx6 = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
270+
ctx6.verify_mode = ssl.CERT_NONE # Noncompliant
271+
272+
ctx7 = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
273+
ctx7.verify_mode = ssl.CERT_NONE # Noncompliant
274+
275+
ctx8 = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) # Compliant
276+
ctx8.verify_mode = ssl.CERT_OPTIONAL
277+
278+
def sslCheckHostNameAttributeSet():
279+
import ssl
280+
281+
ctx1 = ssl._create_unverified_context()
282+
ctx1.check_hostname = True # Compliant: this sets `verify_mode` to CERT_OPTIONAL
283+
284+
ctx2 = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2)
285+
ctx2.check_hostname = True # Compliant
286+
287+
ctx3 = ssl._create_stdlib_context()
288+
ctx3.check_hostname = True # Compliant
289+
290+
ctx4 = ssl.create_default_context()
291+
ctx4.check_hostname = False # Compliant
292+
258293
def httpx_verify():
259294
import httpx
260295

0 commit comments

Comments
 (0)