19
19
*/
20
20
package org .sonar .python .checks .hotspots ;
21
21
22
+ import java .util .ArrayList ;
22
23
import java .util .Arrays ;
23
24
import java .util .HashSet ;
24
25
import java .util .List ;
25
26
import java .util .Locale ;
26
27
import java .util .Optional ;
27
28
import java .util .Set ;
28
29
import java .util .function .Predicate ;
30
+ import java .util .regex .Pattern ;
29
31
import java .util .stream .Collectors ;
30
32
import org .sonar .check .Rule ;
31
33
import org .sonar .plugins .python .api .PythonSubscriptionCheck ;
42
44
import org .sonar .plugins .python .api .tree .KeyValuePair ;
43
45
import org .sonar .plugins .python .api .tree .ListLiteral ;
44
46
import org .sonar .plugins .python .api .tree .Name ;
47
+ import org .sonar .plugins .python .api .tree .QualifiedExpression ;
45
48
import org .sonar .plugins .python .api .tree .RegularArgument ;
46
49
import org .sonar .plugins .python .api .tree .StringLiteral ;
47
50
import org .sonar .plugins .python .api .tree .SubscriptionExpression ;
54
57
@ Rule (key = "S4502" )
55
58
public class CsrfDisabledCheck extends PythonSubscriptionCheck {
56
59
57
- private static final String DISABLING_CSRF_MESSAGE = "Make sure disabling CSRF protection is safe here." ;
58
- private static final String CSRFPROTECT_MISSING_MESSAGE = "Make sure not using CSRFProtect is safe here." ;
60
+ private static final String MESSAGE = "Make sure disabling CSRF protection is safe here." ;
59
61
60
62
@ Override
61
63
public void initialize (Context context ) {
@@ -87,9 +89,7 @@ private static void djangoMiddlewareArrayCheck(SubscriptionContext subscriptionC
87
89
.test (asgn .assignedValue ());
88
90
89
91
if (!containsCsrfViewMiddleware ) {
90
- subscriptionContext .addIssue (
91
- asgn .lastToken (),
92
- "Make sure not using CSRF protection (" + CSRF_VIEW_MIDDLEWARE + ") is safe here." );
92
+ subscriptionContext .addIssue (asgn .lastToken (), MESSAGE );
93
93
}
94
94
}
95
95
}
@@ -128,7 +128,7 @@ private static void decoratorCsrfExemptCheck(SubscriptionContext subscriptionCon
128
128
boolean isDangerous = names .stream ().anyMatch (s -> s .toLowerCase (Locale .US ).contains ("csrf" )) &&
129
129
names .stream ().anyMatch (s -> s .toLowerCase (Locale .US ).contains ("exempt" ));
130
130
if (isDangerous ) {
131
- subscriptionContext .addIssue (decorator .lastToken (), DISABLING_CSRF_MESSAGE );
131
+ subscriptionContext .addIssue (decorator .lastToken (), MESSAGE );
132
132
}
133
133
}
134
134
@@ -138,7 +138,7 @@ private static void functionCsrfExemptCheck(SubscriptionContext subscriptionCont
138
138
Optional .ofNullable (callExpr .calleeSymbol ())
139
139
.map (Symbol ::fullyQualifiedName )
140
140
.filter (DANGEROUS_DECORATORS ::contains )
141
- .ifPresent (fqn -> subscriptionContext .addIssue (callExpr .callee ().lastToken (), DISABLING_CSRF_MESSAGE ));
141
+ .ifPresent (fqn -> subscriptionContext .addIssue (callExpr .callee ().lastToken (), MESSAGE ));
142
142
}
143
143
144
144
/** Checks that <code>'WTF_CSRF_ENABLED'</code> setting is not switched off. */
@@ -154,7 +154,7 @@ private static void flaskWtfCsrfEnabledFalseCheck(SubscriptionContext subscripti
154
154
.flatMap (s -> ((SubscriptionExpression ) s ).subscripts ().expressions ().stream ())
155
155
.anyMatch (isStringSatisfying (s -> "WTF_CSRF_ENABLED" .equals (s ) || "WTF_CSRF_CHECK_DEFAULT" .equals (s )));
156
156
if (isWtfCsrfEnabledSubscription && Expressions .isFalsy (asgn .assignedValue ())) {
157
- subscriptionContext .addIssue (asgn .assignedValue (), DISABLING_CSRF_MESSAGE );
157
+ subscriptionContext .addIssue (asgn .assignedValue (), MESSAGE );
158
158
}
159
159
}
160
160
@@ -182,7 +182,7 @@ private static void metaCheck(SubscriptionContext subscriptionContext) {
182
182
if (stmt .is (Tree .Kind .ASSIGNMENT_STMT )) {
183
183
AssignmentStatement asgn = (AssignmentStatement ) stmt ;
184
184
if (isLhsCalled ("csrf" ).test (asgn ) && Expressions .isFalsy (asgn .assignedValue ())) {
185
- subscriptionContext .addIssue (asgn .assignedValue (), DISABLING_CSRF_MESSAGE );
185
+ subscriptionContext .addIssue (asgn .assignedValue (), MESSAGE );
186
186
}
187
187
}
188
188
});
@@ -204,7 +204,7 @@ private static void formInstantiationCheck(SubscriptionContext subscriptionConte
204
204
if (arg instanceof RegularArgument ) {
205
205
RegularArgument regArg = (RegularArgument ) arg ;
206
206
searchForProblemsInFormInitializationArguments (regArg )
207
- .ifPresent (badExpr -> subscriptionContext .addIssue (badExpr , DISABLING_CSRF_MESSAGE ));
207
+ .ifPresent (badExpr -> subscriptionContext .addIssue (badExpr , MESSAGE ));
208
208
}
209
209
});
210
210
}
@@ -252,7 +252,7 @@ private static void improperlyConfiguredFlaskApp(SubscriptionContext subscriptio
252
252
.flatMap (usages -> usages .stream ().filter (CsrfDisabledCheck ::isWithinCsrfEnablingStatement ).findFirst ()))
253
253
.isPresent ();
254
254
if (!isCsrfEnabledInThisFile ) {
255
- subscriptionContext .addIssue (asgn .assignedValue (), CSRFPROTECT_MISSING_MESSAGE );
255
+ subscriptionContext .addIssue (asgn .assignedValue (), MESSAGE );
256
256
}
257
257
}
258
258
}
@@ -266,19 +266,76 @@ private static boolean isFlaskAppInstantiation(Expression expr) {
266
266
return false ;
267
267
}
268
268
269
+
270
+
271
+ /** Attempts to extract a list of name fragments from a nested qualified expressions. */
272
+ private static Optional <ArrayList <String >> extractQualifiedNameComponents (Expression expr ) {
273
+ if (expr .is (Tree .Kind .NAME )) {
274
+ ArrayList <String > res = new ArrayList <>();
275
+ res .add (((Name ) expr ).name ());
276
+ return Optional .of (res );
277
+ } else if (expr .is (Tree .Kind .QUALIFIED_EXPR )){
278
+ QualifiedExpression qe = (QualifiedExpression ) expr ;
279
+ return extractQualifiedNameComponents (qe .qualifier ()).map (list -> { list .add (qe .name ().name ()); return list ; });
280
+ } else {
281
+ return Optional .empty ();
282
+ }
283
+ }
284
+
285
+ private static final List <Pattern > CSRF_INIT_APP_CALLEE_PATTERNS = Arrays .asList (
286
+ Pattern .compile ("(csrf|CSRF)" ),
287
+ Pattern .compile ("init_app" )
288
+ );
289
+
290
+ /**
291
+ * Attempts to unpack the <code>expr</code> as nested <code>QualifiedExpression</code>s, and checks that
292
+ * every component of the name matches the corresponding regex pattern.
293
+ */
294
+ private static boolean checkNestedQualifiedExpressions (List <Pattern > patternsToMatch , Expression expr ) {
295
+ Optional <ArrayList <String >> nameFragmentsOpt = extractQualifiedNameComponents (expr );
296
+ return nameFragmentsOpt .filter (nameFragments -> {
297
+ if (nameFragments .size () == patternsToMatch .size ()) {
298
+ for (int i = 0 ; i < nameFragments .size (); i ++) {
299
+ Pattern p = patternsToMatch .get (i );
300
+ String s = nameFragments .get (i );
301
+ if (!p .matcher (s ).matches ()) {
302
+ return false ;
303
+ }
304
+ }
305
+ return true ;
306
+ } else {
307
+ return false ;
308
+ }
309
+ }).isPresent ();
310
+ }
311
+
269
312
/** Detects usages like <code>CSRFProtect(a)</code>. */
270
313
private static boolean isWithinCsrfEnablingStatement (Usage u ) {
271
314
Tree t = u .tree ();
272
- return isWithinCall ("flask_wtf.csrf.CSRFProtect" , t ) ||
273
- isWithinCall ("flask_wtf.csrf.CSRFProtect.init_app" , t );
315
+ return isWithinCall (new HashSet <>(Arrays .asList (
316
+ "flask_wtf.csrf.CSRFProtect" ,
317
+ "flask_wtf.csrf.CSRFProtect.init_app" ,
318
+ "flask_wtf.CSRFProtect" ,
319
+ "flask_wtf.CSRFProtect.init_app"
320
+ )), CSRF_INIT_APP_CALLEE_PATTERNS , t );
274
321
}
275
322
276
- /** Checks that the surroundings of <code>t</code> look like <code>expectedCalleeFqn(someExpr(t))</code>. */
277
- private static boolean isWithinCall (String expectedCalleeFqn , Tree t ) {
323
+ /**
324
+ * Checks that the surroundings of <code>t</code> look like <code>expectedCallee(someExpr(t))</code>,
325
+ * where the <code>expectedCallee</code> is either a symbol with an FQN from the specified set,
326
+ * or where at least the name of the callee matches a given regex.
327
+ */
328
+ @ SuppressWarnings ("SameParameterValue" )
329
+ private static boolean isWithinCall (Set <String > expectedCalleeFqns , List <Pattern > fallbackCalleeRegexes , Tree t ) {
278
330
Tree callExprTree = TreeUtils .firstAncestorOfKind (t , Tree .Kind .CALL_EXPR );
279
331
if (callExprTree != null ) {
280
332
Symbol callExprSymb = ((CallExpression ) callExprTree ).calleeSymbol ();
281
- return callExprSymb != null && expectedCalleeFqn .equals (callExprSymb .fullyQualifiedName ());
333
+ if (callExprSymb != null && expectedCalleeFqns .contains (callExprSymb .fullyQualifiedName ())) {
334
+ return true ;
335
+ } else {
336
+ Expression callee = ((CallExpression ) callExprTree ).callee ();
337
+ return checkNestedQualifiedExpressions (fallbackCalleeRegexes , callee );
338
+ }
282
339
}
283
340
return false ;
284
341
}
0 commit comments