1919import java .util .Arrays ;
2020import java .util .Collections ;
2121import java .util .HashSet ;
22+ import java .util .Objects ;
23+ import java .util .Optional ;
2224import java .util .Set ;
25+ import java .util .function .Predicate ;
2326import org .sonar .check .Rule ;
2427import org .sonar .plugins .python .api .PythonSubscriptionCheck ;
2528import org .sonar .plugins .python .api .SubscriptionContext ;
2932import org .sonar .plugins .python .api .tree .CallExpression ;
3033import org .sonar .plugins .python .api .tree .Expression ;
3134import org .sonar .plugins .python .api .tree .HasSymbol ;
35+ import org .sonar .plugins .python .api .tree .Name ;
3236import org .sonar .plugins .python .api .tree .QualifiedExpression ;
3337import org .sonar .plugins .python .api .tree .RegularArgument ;
3438import org .sonar .plugins .python .api .tree .Tree ;
3539import org .sonar .plugins .python .api .types .v2 .PythonType ;
3640import org .sonar .plugins .python .api .types .v2 .TriBool ;
3741import org .sonar .python .checks .utils .Expressions ;
42+ import org .sonar .python .semantic .v2 .SymbolV2 ;
43+ import org .sonar .python .semantic .v2 .UsageV2 ;
3844import org .sonar .python .tree .TreeUtils ;
3945import 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+
0 commit comments