Skip to content

Commit 08670b1

Browse files
SONARPY-668 Minor fixes: flask_wtf.CSRFProtect imports, csrf.init_app for unknown symbols (#704)
1 parent 85af5de commit 08670b1

File tree

6 files changed

+116
-18
lines changed

6 files changed

+116
-18
lines changed

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

Lines changed: 73 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,15 @@
1919
*/
2020
package org.sonar.python.checks.hotspots;
2121

22+
import java.util.ArrayList;
2223
import java.util.Arrays;
2324
import java.util.HashSet;
2425
import java.util.List;
2526
import java.util.Locale;
2627
import java.util.Optional;
2728
import java.util.Set;
2829
import java.util.function.Predicate;
30+
import java.util.regex.Pattern;
2931
import java.util.stream.Collectors;
3032
import org.sonar.check.Rule;
3133
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
@@ -42,6 +44,7 @@
4244
import org.sonar.plugins.python.api.tree.KeyValuePair;
4345
import org.sonar.plugins.python.api.tree.ListLiteral;
4446
import org.sonar.plugins.python.api.tree.Name;
47+
import org.sonar.plugins.python.api.tree.QualifiedExpression;
4548
import org.sonar.plugins.python.api.tree.RegularArgument;
4649
import org.sonar.plugins.python.api.tree.StringLiteral;
4750
import org.sonar.plugins.python.api.tree.SubscriptionExpression;
@@ -54,8 +57,7 @@
5457
@Rule(key = "S4502")
5558
public 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
}

python-checks/src/test/java/org/sonar/python/checks/hotspots/CsrfDisabledCheckTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,11 @@ public void testExemptDecorators() {
6464
public void testExemptAsFunction() {
6565
testFile("flask/exemptAsFunction.py");
6666
}
67+
68+
@Test
69+
public void fixupTestsMoreRobustCSRFProtect() { testFile("flask/fixupTestsMoreRobustCSRFProtect.py"); }
70+
71+
@Test
72+
public void fixupCsrfInGlobalScope() { testFile("flask/fixupCsrfInGlobalScope.py"); }
73+
6774
}

python-checks/src/test/resources/checks/hotspots/csrfDisabledCheck/django/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
'django.middleware.security.SecurityMiddleware',
33
# 'django.middleware.csrf.CsrfViewMiddleware',
44
'django.middleware.clickjacking.XFrameOptionsMiddleware',
5-
] # Noncompliant {{Make sure not using CSRF protection (django.middleware.csrf.CsrfViewMiddleware) is safe here.}}
5+
] # Noncompliant {{Make sure disabling CSRF protection is safe here.}}
66

77
MIDDLEWARE = [
88
'django.middleware.security.SecurityMiddleware',
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from flask import Flask
2+
from flask_wtf import CSRFProtect
3+
csrf = CSRFProtect()
4+
def create_app():
5+
app = Flask(__name__) # Compliant
6+
csrf.init_app(app)
7+
8+
def create_app_noncompliant():
9+
app2 = Flask(__name__) # Noncompliant
10+
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
def csrfInitAppShouldWorkEvenIfCsrfSymbolIsNotFound():
2+
from flask import Flask
3+
app = Flask(__name__) # Compliant
4+
csrf.init_app(app) # Unknown symbol, but looks similar enough to CSRFProtect.
5+
6+
def csrfInitAppShouldWorkEvenIfCsrfSymbolIsNotFoundUppercase():
7+
from flask import Flask
8+
app = Flask(__name__) # Compliant
9+
CSRF.init_app(app) # Unknown symbol, but looks similar enough to CSRFProtect.
10+
11+
def csrfInitAppShouldCheckTheQualifier():
12+
from flask import Flask
13+
app = Flask(__name__) # Noncompliant {{Make sure disabling CSRF protection is safe here.}}
14+
# ^^^^^^^^^^^^^^^
15+
somethingUnrelated.init_app(app) # insufficient
16+
tooLong.csrf.init_app(app) # insufficient
17+
csrf.do_something_else(app) # insufficient
18+
19+
def csrfProtectCanBeImportedFromFlaskWtfDirectly():
20+
from flask import Flask
21+
from flask_wtf import CSRFProtect
22+
app = Flask(__name__) # Compliant
23+
CSRFProtect(app)
24+

python-checks/src/test/resources/checks/hotspots/csrfDisabledCheck/flask/global.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ def misconfiguredFlaskExamples():
33
from flask_wtf.csrf import CSRFProtect
44
# from flask_wtf import csrf
55

6-
app1 = Flask(__name__) # Noncompliant {{Make sure not using CSRFProtect is safe here.}}
6+
app1 = Flask(__name__) # Noncompliant {{Make sure disabling CSRF protection is safe here.}}
77
# ^^^^^^^^^^^^^^^
88

99
app2 = Flask(__name__)

0 commit comments

Comments
 (0)