Skip to content

Commit 56d14aa

Browse files
ghislainpiotsonartech
authored andcommitted
SONARPY-1705 Add support for Django to S5344 (#192)
GitOrigin-RevId: 0b6c5f152c2f78874a6d01e5a4e6f2385e1db925
1 parent 3cba41d commit 56d14aa

6 files changed

Lines changed: 86 additions & 5 deletions

File tree

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,17 @@
1919
import java.util.Collection;
2020
import java.util.List;
2121
import java.util.Map;
22+
import java.util.Optional;
2223
import java.util.Set;
24+
import java.util.stream.Stream;
2325
import org.sonar.check.Rule;
2426
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
2527
import org.sonar.plugins.python.api.SubscriptionContext;
2628
import org.sonar.plugins.python.api.tree.AssignmentStatement;
2729
import org.sonar.plugins.python.api.tree.CallExpression;
2830
import org.sonar.plugins.python.api.tree.Expression;
2931
import org.sonar.plugins.python.api.tree.ExpressionList;
32+
import org.sonar.plugins.python.api.tree.ListLiteral;
3033
import org.sonar.plugins.python.api.tree.Name;
3134
import org.sonar.plugins.python.api.tree.QualifiedExpression;
3235
import org.sonar.plugins.python.api.tree.RegularArgument;
@@ -54,12 +57,21 @@ public class FastHashingOrPlainTextCheck extends PythonSubscriptionCheck {
5457
private static final String PBKDF2_MESSAGE = "Use at least 100 000 iterations.";
5558
private static final String ARGON2_MESSAGE = "Use secure Argon2 parameters.";
5659
private static final String BCRYPT_MESSAGE = "Use strong bcrypt parameters.";
60+
private static final String DJANGO_MESSAGE = "Use a secure hashing algorithm to store passwords.";
61+
5762
private static final Set<String> PBKDF2_ALGOS = Set.of(
5863
"sha1",
5964
"sha256",
6065
"sha512"
6166
);
6267
private static final String ROUNDS = "rounds";
68+
private static final Set<String> DJANGO_FIRST_FORBIDDEN_HASHERS = Set.of(
69+
"django.contrib.auth.hashers.SHA1PasswordHasher",
70+
"django.contrib.auth.hashers.MD5PasswordHasher",
71+
"django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher",
72+
"django.contrib.auth.hashers.UnsaltedMD5PasswordHasher",
73+
"django.contrib.auth.hashers.CryptPasswordHasher"
74+
);
6375

6476

6577
private static final ArgumentValidator SCRYPT_R = new ArgumentValidator(
@@ -201,11 +213,39 @@ public class FastHashingOrPlainTextCheck extends PythonSubscriptionCheck {
201213
@Override
202214
public void initialize(Context context) {
203215
context.registerSyntaxNodeConsumer(Tree.Kind.FILE_INPUT, this::registerTypeCheckers);
216+
context.registerSyntaxNodeConsumer(Tree.Kind.FILE_INPUT, subscriptionContext1 -> {
217+
if (!"settings.py".equals(subscriptionContext1.pythonFile().fileName())) {
218+
return;
219+
}
220+
context.registerSyntaxNodeConsumer(Tree.Kind.ASSIGNMENT_STMT, FastHashingOrPlainTextCheck::checkDjangoHasher);
221+
});
204222
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, FastHashingOrPlainTextCheck::checkCallExpr);
205223
context.registerSyntaxNodeConsumer(Tree.Kind.NAME, this::checkName);
206224
context.registerSyntaxNodeConsumer(Tree.Kind.ASSIGNMENT_STMT, subscriptionContext -> checkAssignment(subscriptionContext, flaskConfigTypeChecker));
207225
}
208226

227+
private static void checkDjangoHasher(SubscriptionContext subscriptionContext) {
228+
var stmt = (AssignmentStatement) subscriptionContext.syntaxNode();
229+
var lhsIsConfig = stmt.lhsExpressions().stream().findFirst()
230+
.map(ExpressionList::expressions)
231+
.flatMap(list -> list.stream().findFirst())
232+
.filter(expression -> expression.is(Tree.Kind.NAME))
233+
.filter(name -> "PASSWORD_HASHERS".equals(((Name) name).name()));
234+
235+
var firstRhsString = Optional.of(stmt.assignedValue())
236+
.flatMap(TreeUtils.toOptionalInstanceOfMapper(ListLiteral.class))
237+
.map(ListLiteral::elements)
238+
.map(ExpressionList::expressions)
239+
.map(List::stream)
240+
.flatMap(Stream::findFirst)
241+
.flatMap(TreeUtils.toOptionalInstanceOfMapper(StringLiteral.class))
242+
.filter(stringLiteral -> DJANGO_FIRST_FORBIDDEN_HASHERS.contains(stringLiteral.trimmedQuotesValue()));
243+
244+
if (lhsIsConfig.isPresent() && firstRhsString.isPresent()) {
245+
subscriptionContext.addIssue(firstRhsString.get(), DJANGO_MESSAGE);
246+
}
247+
}
248+
209249
private void registerTypeCheckers(SubscriptionContext subscriptionContext) {
210250
argon2CheapestProfileTypeChecker = subscriptionContext.typeChecker().typeCheckBuilder().isTypeWithFqn("argon2.profiles.CHEAPEST");
211251
flaskConfigTypeChecker = subscriptionContext.typeChecker().typeCheckBuilder().isInstanceOf("flask.config.Config");

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,24 +16,30 @@
1616
*/
1717
package org.sonar.python.checks.hotspots;
1818

19-
import java.util.Collections;
2019
import org.junit.jupiter.api.Test;
2120
import org.sonar.python.checks.utils.PythonCheckVerifier;
2221

2322
class FastHashingOrPlainTextCheckTest {
2423
@Test
2524
void test() {
26-
PythonCheckVerifier.verify(Collections.singletonList("src/test/resources/checks/fastHashingOrPlainText.py"), new FastHashingOrPlainTextCheck());
25+
PythonCheckVerifier.verify("src/test/resources/checks/fastHashingOrPlainText/fastHashingOrPlainText.py", new FastHashingOrPlainTextCheck());
2726
}
2827

2928
@Test
3029
void cheapestAnywhereImportFrom() {
31-
PythonCheckVerifier.verify(Collections.singletonList("src/test/resources/checks/fastHashingOrPlainTextArgon2ImportFrom.py"), new FastHashingOrPlainTextCheck());
30+
PythonCheckVerifier.verify("src/test/resources/checks/fastHashingOrPlainText/fastHashingOrPlainTextArgon2ImportFrom.py",
31+
new FastHashingOrPlainTextCheck());
3232
}
3333

3434
@Test
3535
void cheapestAnywhereImport() {
36-
PythonCheckVerifier.verify(Collections.singletonList("src/test/resources/checks/fastHashingOrPlainTextArgon2Import.py"), new FastHashingOrPlainTextCheck());
36+
PythonCheckVerifier.verify("src/test/resources/checks/fastHashingOrPlainText/fastHashingOrPlainTextArgon2Import.py",
37+
new FastHashingOrPlainTextCheck());
3738
}
3839

39-
}
40+
@Test
41+
void djangoSettings() {
42+
PythonCheckVerifier.verify("src/test/resources/checks/fastHashingOrPlainText/settings.py", new FastHashingOrPlainTextCheck());
43+
}
44+
45+
}

python-checks/src/test/resources/checks/fastHashingOrPlainText.py renamed to python-checks/src/test/resources/checks/fastHashingOrPlainText/fastHashingOrPlainText.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,3 +345,14 @@ def flask_config():
345345

346346
some_dict = {}
347347
some_dict['BCRYPT_LOG_ROUNDS'] = 11
348+
349+
350+
## Django
351+
PASSWORD_HASHERS = [
352+
"django.contrib.auth.hashers.Argon2PasswordHasher",
353+
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher"
354+
]
355+
356+
PASSWORD_HASHERS = [
357+
"django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher", # OK outside of settings.py
358+
]

python-checks/src/test/resources/checks/fastHashingOrPlainTextArgon2Import.py renamed to python-checks/src/test/resources/checks/fastHashingOrPlainText/fastHashingOrPlainTextArgon2Import.py

File renamed without changes.

python-checks/src/test/resources/checks/fastHashingOrPlainTextArgon2ImportFrom.py renamed to python-checks/src/test/resources/checks/fastHashingOrPlainText/fastHashingOrPlainTextArgon2ImportFrom.py

File renamed without changes.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
PASSWORD_HASHERS = [
2+
"django.contrib.auth.hashers.Argon2PasswordHasher", # Compliant
3+
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
4+
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
5+
"django.contrib.auth.hashers.BCryptSHA256PasswordHasher",
6+
"django.contrib.auth.hashers.ScryptPasswordHasher",
7+
]
8+
9+
PASSWORD_HASHERS = [
10+
"django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher", # Noncompliant {{Use a secure hashing algorithm to store passwords.}}
11+
]
12+
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^@-1
13+
14+
NOT_A_HASHER = []
15+
16+
PASSWORD_HASHERS = [
17+
"django.contrib.auth.hashers.ScryptPasswordHasher",
18+
"django.contrib.auth.hashers.UnsaltedSHA1PasswordHasher"
19+
]
20+
21+
PASSWORD_HASHERS = [
22+
"django.contrib.auth.hashers.CryptPasswordHasher", # Noncompliant {{Use a secure hashing algorithm to store passwords.}}
23+
"django.contrib.auth.hashers.ScryptPasswordHasher",
24+
]

0 commit comments

Comments
 (0)