1919 */
2020package org .sonar .python .checks .hotspots ;
2121
22+ import java .util .ArrayList ;
2223import java .util .Arrays ;
2324import java .util .HashSet ;
2425import java .util .List ;
2526import java .util .Locale ;
2627import java .util .Optional ;
2728import java .util .Set ;
2829import java .util .function .Predicate ;
30+ import java .util .regex .Pattern ;
2931import java .util .stream .Collectors ;
3032import org .sonar .check .Rule ;
3133import org .sonar .plugins .python .api .PythonSubscriptionCheck ;
4244import org .sonar .plugins .python .api .tree .KeyValuePair ;
4345import org .sonar .plugins .python .api .tree .ListLiteral ;
4446import org .sonar .plugins .python .api .tree .Name ;
47+ import org .sonar .plugins .python .api .tree .QualifiedExpression ;
4548import org .sonar .plugins .python .api .tree .RegularArgument ;
4649import org .sonar .plugins .python .api .tree .StringLiteral ;
4750import org .sonar .plugins .python .api .tree .SubscriptionExpression ;
5457@ Rule (key = "S4502" )
5558public class CsrfDisabledCheck extends PythonSubscriptionCheck {
5659
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." ;
5961
6062 @ Override
6163 public void initialize (Context context ) {
@@ -87,9 +89,7 @@ private static void djangoMiddlewareArrayCheck(SubscriptionContext subscriptionC
8789 .test (asgn .assignedValue ());
8890
8991 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 );
9393 }
9494 }
9595 }
@@ -128,7 +128,7 @@ private static void decoratorCsrfExemptCheck(SubscriptionContext subscriptionCon
128128 boolean isDangerous = names .stream ().anyMatch (s -> s .toLowerCase (Locale .US ).contains ("csrf" )) &&
129129 names .stream ().anyMatch (s -> s .toLowerCase (Locale .US ).contains ("exempt" ));
130130 if (isDangerous ) {
131- subscriptionContext .addIssue (decorator .lastToken (), DISABLING_CSRF_MESSAGE );
131+ subscriptionContext .addIssue (decorator .lastToken (), MESSAGE );
132132 }
133133 }
134134
@@ -138,7 +138,7 @@ private static void functionCsrfExemptCheck(SubscriptionContext subscriptionCont
138138 Optional .ofNullable (callExpr .calleeSymbol ())
139139 .map (Symbol ::fullyQualifiedName )
140140 .filter (DANGEROUS_DECORATORS ::contains )
141- .ifPresent (fqn -> subscriptionContext .addIssue (callExpr .callee ().lastToken (), DISABLING_CSRF_MESSAGE ));
141+ .ifPresent (fqn -> subscriptionContext .addIssue (callExpr .callee ().lastToken (), MESSAGE ));
142142 }
143143
144144 /** Checks that <code>'WTF_CSRF_ENABLED'</code> setting is not switched off. */
@@ -154,7 +154,7 @@ private static void flaskWtfCsrfEnabledFalseCheck(SubscriptionContext subscripti
154154 .flatMap (s -> ((SubscriptionExpression ) s ).subscripts ().expressions ().stream ())
155155 .anyMatch (isStringSatisfying (s -> "WTF_CSRF_ENABLED" .equals (s ) || "WTF_CSRF_CHECK_DEFAULT" .equals (s )));
156156 if (isWtfCsrfEnabledSubscription && Expressions .isFalsy (asgn .assignedValue ())) {
157- subscriptionContext .addIssue (asgn .assignedValue (), DISABLING_CSRF_MESSAGE );
157+ subscriptionContext .addIssue (asgn .assignedValue (), MESSAGE );
158158 }
159159 }
160160
@@ -182,7 +182,7 @@ private static void metaCheck(SubscriptionContext subscriptionContext) {
182182 if (stmt .is (Tree .Kind .ASSIGNMENT_STMT )) {
183183 AssignmentStatement asgn = (AssignmentStatement ) stmt ;
184184 if (isLhsCalled ("csrf" ).test (asgn ) && Expressions .isFalsy (asgn .assignedValue ())) {
185- subscriptionContext .addIssue (asgn .assignedValue (), DISABLING_CSRF_MESSAGE );
185+ subscriptionContext .addIssue (asgn .assignedValue (), MESSAGE );
186186 }
187187 }
188188 });
@@ -204,7 +204,7 @@ private static void formInstantiationCheck(SubscriptionContext subscriptionConte
204204 if (arg instanceof RegularArgument ) {
205205 RegularArgument regArg = (RegularArgument ) arg ;
206206 searchForProblemsInFormInitializationArguments (regArg )
207- .ifPresent (badExpr -> subscriptionContext .addIssue (badExpr , DISABLING_CSRF_MESSAGE ));
207+ .ifPresent (badExpr -> subscriptionContext .addIssue (badExpr , MESSAGE ));
208208 }
209209 });
210210 }
@@ -252,7 +252,7 @@ private static void improperlyConfiguredFlaskApp(SubscriptionContext subscriptio
252252 .flatMap (usages -> usages .stream ().filter (CsrfDisabledCheck ::isWithinCsrfEnablingStatement ).findFirst ()))
253253 .isPresent ();
254254 if (!isCsrfEnabledInThisFile ) {
255- subscriptionContext .addIssue (asgn .assignedValue (), CSRFPROTECT_MISSING_MESSAGE );
255+ subscriptionContext .addIssue (asgn .assignedValue (), MESSAGE );
256256 }
257257 }
258258 }
@@ -266,19 +266,76 @@ private static boolean isFlaskAppInstantiation(Expression expr) {
266266 return false ;
267267 }
268268
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+
269312 /** Detects usages like <code>CSRFProtect(a)</code>. */
270313 private static boolean isWithinCsrfEnablingStatement (Usage u ) {
271314 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 );
274321 }
275322
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 ) {
278330 Tree callExprTree = TreeUtils .firstAncestorOfKind (t , Tree .Kind .CALL_EXPR );
279331 if (callExprTree != null ) {
280332 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+ }
282339 }
283340 return false ;
284341 }
0 commit comments