Skip to content

Commit cee48af

Browse files
guillaume-dequennesonartech
authored andcommitted
SONARPY-1709 S5527 Add PyOpenSSL support (#194)
GitOrigin-RevId: 8fc6a751314e59aeb12d055deb919099ff99ab67
1 parent 56d14aa commit cee48af

5 files changed

Lines changed: 73 additions & 8 deletions

File tree

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

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,20 +30,28 @@
3030
import org.sonar.plugins.python.api.tree.Expression;
3131
import org.sonar.plugins.python.api.tree.HasSymbol;
3232
import org.sonar.plugins.python.api.tree.QualifiedExpression;
33+
import org.sonar.plugins.python.api.tree.RegularArgument;
3334
import org.sonar.plugins.python.api.tree.Tree;
35+
import org.sonar.plugins.python.api.types.v2.PythonType;
36+
import org.sonar.plugins.python.api.types.v2.TriBool;
3437
import org.sonar.python.checks.utils.Expressions;
3538
import org.sonar.python.tree.TreeUtils;
39+
import org.sonar.python.types.v2.TypeCheckBuilder;
3640

3741
@Rule(key = "S5527")
3842
public class UnverifiedHostnameCheck extends PythonSubscriptionCheck {
3943

4044
private static final String MESSAGE = "Enable server hostname verification on this SSL/TLS connection.";
45+
private static final String SECONDARY_OPENSSL = "This context does not perform hostname verification.";
4146

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

4550
private static Set<String> functionsToCheck;
4651

52+
private TypeCheckBuilder openSSLConnectionTypeCheckBuilder;
53+
private TypeCheckBuilder openSSLContextTypeCheckBuilder;
54+
4755
private static Set<String> functionsToCheck() {
4856
if (functionsToCheck == null) {
4957
functionsToCheck = new HashSet<>();
@@ -97,17 +105,43 @@ private static boolean opensUnsecureConnection(Symbol calleeSymbol, CallExpressi
97105

98106
@Override
99107
public void initialize(Context context) {
100-
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, UnverifiedHostnameCheck::checkCallExpression);
108+
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, this::checkCallExpression);
109+
context.registerSyntaxNodeConsumer(Tree.Kind.FILE_INPUT, ctx -> {
110+
openSSLConnectionTypeCheckBuilder = ctx.typeChecker().typeCheckBuilder().isTypeWithName("OpenSSL.SSL.Connection");
111+
openSSLContextTypeCheckBuilder = ctx.typeChecker().typeCheckBuilder().isInstanceOf("OpenSSL.SSL.Context");
112+
});
101113
}
102114

103-
private static void checkCallExpression(SubscriptionContext ctx) {
115+
private void checkCallExpression(SubscriptionContext ctx) {
104116
CallExpression callExpression = (CallExpression) ctx.syntaxNode();
105117
Symbol calleeSymbol = callExpression.calleeSymbol();
106-
if (calleeSymbol == null) {
118+
if (calleeSymbol != null && functionsToCheck().contains(calleeSymbol.fullyQualifiedName())) {
119+
checkSuspiciousCall(callExpression, calleeSymbol, ctx);
120+
}
121+
checkOpenSSLConnection(ctx, callExpression);
122+
}
123+
124+
private void checkOpenSSLConnection(SubscriptionContext ctx, CallExpression callExpression) {
125+
Expression callee = callExpression.callee();
126+
PythonType pythonType = callee.typeV2();
127+
128+
if (openSSLConnectionTypeCheckBuilder.check(pythonType) != TriBool.TRUE) {
107129
return;
108130
}
109-
if (functionsToCheck().contains(calleeSymbol.fullyQualifiedName())) {
110-
checkSuspiciousCall(callExpression, calleeSymbol, ctx);
131+
132+
RegularArgument contextArg = TreeUtils.nthArgumentOrKeyword(0, "context", callExpression.arguments());
133+
if (contextArg == null) {
134+
return;
135+
}
136+
137+
Expression contextExpr = contextArg.expression();
138+
PythonType contextType = contextExpr.typeV2();
139+
if (openSSLContextTypeCheckBuilder.check(contextType) == TriBool.TRUE) {
140+
PreciseIssue issue = ctx.addIssue(callee, MESSAGE);
141+
Expressions.ifNameGetSingleAssignedNonNameValue(contextExpr)
142+
.ifPresentOrElse(e -> issue.secondary(e, SECONDARY_OPENSSL),
143+
() -> issue.secondary(contextExpr, SECONDARY_OPENSSL)
144+
);
111145
}
112146
}
113147
}

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,28 @@
4646

4747
if ssl._create_unverified_context() == whatever:
4848
pass
49+
50+
def pyopenssl_noncompliant():
51+
import socket
52+
from OpenSSL import SSL
53+
54+
ctx = SSL.Context(SSL.TLSv1_2_METHOD)
55+
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^> {{This context does not perform hostname verification.}}
56+
ctx.set_verify(SSL.VERIFY_PEER)
57+
58+
conn = SSL.Connection(ctx, socket.socket(socket.AF_INET, socket.SOCK_STREAM)) # Noncompliant {{Enable server hostname verification on this SSL/TLS connection.}}
59+
# ^^^^^^^^^^^^^^
60+
61+
SSL.Connection(SSL.Context(SSL.TLSv1_2_METHOD)) # Noncompliant
62+
# ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^<
63+
64+
if foo:
65+
ctx2 = SSL.Context(SSL.TLSv1_2_METHOD)
66+
else:
67+
ctx2 = SSL.Context(SSL.TLSv1_3_METHOD)
68+
SSL.Connection(ctx2) # Noncompliant
69+
# ^^^^^^^^^^^^^^ ^^^^<
70+
71+
ctx3 = foo()
72+
SSL.Connection() # OK, no context argument
73+
SSL.Connection(ctx3)

python-frontend/src/main/resources/org/sonar/python/types/custom_protobuf/OpenSSL.SSL.protobuf

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ set_verifyOpenSSL.SSL.Context.set_verify"
99
args
1010
Any*
1111
kwargs
12-
Any*�
12+
AnyP
13+
14+
ConnectionOpenSSL.SSL.Connection"*SonarPythonAnalyzerFakeStub.CustomStubBase*�
1315
__annotations__OpenSSL.SSL.__annotations__W
1416
builtins.dict[builtins.str,Any]
1517
builtins.str" builtins.str
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
28d817d04de5e4f99762476c6c950e6f0c9c5fa91af954afa9b1d44eab3102a5
2-
bbcc49c75d53b7cc9fd2135583b435385795dddb5696ac8094ed88440da841de
1+
cb2ccbe2913935c2c4dfdb7d09b8f40c6d2a2ec9840a74573f0cc6adb6ab6e69
2+
3c1a63e0fbd63e97ebbbd0eb13d3c27c3f9051aaac329868be6e90ccdc31546c

python-frontend/typeshed_serializer/resources/custom/OpenSSL/SSL.pyi

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@ VERIFY_NONE: int
66

77
class Context(CustomStubBase):
88
def set_verify(self, *args, **kwargs) -> None: ...
9+
10+
11+
class Connection(CustomStubBase):
12+
...

0 commit comments

Comments
 (0)