Skip to content

Commit 46d7aaa

Browse files
ghislainpiotsonartech
authored andcommitted
SONARPY-1702 Add support for bcrypt to S5344 (#191)
GitOrigin-RevId: 683a06d303003973bec9cfc510352ff2762f66b0
1 parent 713fb78 commit 46d7aaa

2 files changed

Lines changed: 114 additions & 5 deletions

File tree

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

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@
2626
import org.sonar.plugins.python.api.tree.AssignmentStatement;
2727
import org.sonar.plugins.python.api.tree.CallExpression;
2828
import org.sonar.plugins.python.api.tree.Expression;
29+
import org.sonar.plugins.python.api.tree.ExpressionList;
2930
import org.sonar.plugins.python.api.tree.Name;
3031
import org.sonar.plugins.python.api.tree.QualifiedExpression;
3132
import org.sonar.plugins.python.api.tree.RegularArgument;
3233
import org.sonar.plugins.python.api.tree.StringLiteral;
34+
import org.sonar.plugins.python.api.tree.SubscriptionExpression;
3335
import org.sonar.plugins.python.api.tree.Tree;
3436
import org.sonar.plugins.python.api.types.v2.TriBool;
3537
import org.sonar.python.checks.hotspots.CommonValidationUtils.ArgumentValidator;
@@ -40,6 +42,7 @@
4042

4143
import static org.sonar.python.checks.hotspots.CommonValidationUtils.isEqualTo;
4244
import static org.sonar.python.checks.hotspots.CommonValidationUtils.isLessThan;
45+
import static org.sonar.python.checks.hotspots.CommonValidationUtils.isLessThanExponent;
4346
import static org.sonar.python.semantic.SymbolUtils.qualifiedNameOrEmpty;
4447
import static org.sonar.python.tree.TreeUtils.nthArgumentOrKeyword;
4548
import static org.sonar.python.tree.TreeUtils.nthArgumentOrKeywordOptional;
@@ -50,6 +53,7 @@ public class FastHashingOrPlainTextCheck extends PythonSubscriptionCheck {
5053
private static final String SCRYPT_PARAMETERS_MESSAGE = "Use strong scrypt parameters.";
5154
private static final String PBKDF2_MESSAGE = "Use at least 100 000 iterations.";
5255
private static final String ARGON2_MESSAGE = "Use secure Argon2 parameters.";
56+
private static final String BCRYPT_MESSAGE = "Use strong bcrypt parameters.";
5357
private static final Set<String> PBKDF2_ALGOS = Set.of(
5458
"sha1",
5559
"sha256",
@@ -72,7 +76,7 @@ public class FastHashingOrPlainTextCheck extends PythonSubscriptionCheck {
7276
});
7377
private static final ArgumentValidator SCRYPT_N = new ArgumentValidator(
7478
2, "N", (ctx, argument) -> {
75-
if (isLessThan(argument.expression(), (int) Math.pow(2, 13)) || CommonValidationUtils.isLessThanExponent(argument.expression(), 13)) {
79+
if (isLessThan(argument.expression(), (int) Math.pow(2, 13)) || isLessThanExponent(argument.expression(), 13)) {
7680
ctx.addIssue(argument, SCRYPT_PARAMETERS_MESSAGE);
7781
}
7882
});
@@ -85,7 +89,7 @@ public class FastHashingOrPlainTextCheck extends PythonSubscriptionCheck {
8589
});
8690
private static final ArgumentValidator HASHLIB_N = new ArgumentValidator(
8791
2, "n", (ctx, argument) -> {
88-
if (isLessThan(argument.expression(), (int) Math.pow(2, 13)) || CommonValidationUtils.isLessThanExponent(argument.expression(), 13)) {
92+
if (isLessThan(argument.expression(), (int) Math.pow(2, 13)) || isLessThanExponent(argument.expression(), 13)) {
8993
ctx.addIssue(argument, SCRYPT_PARAMETERS_MESSAGE);
9094
}
9195
});
@@ -104,7 +108,7 @@ public class FastHashingOrPlainTextCheck extends PythonSubscriptionCheck {
104108
});
105109
private static final ArgumentValidator CRYPTOGRAPHY_N = new ArgumentValidator(
106110
2, "n", (ctx, argument) -> {
107-
if (isLessThan(argument.expression(), (int) Math.pow(2, 13)) || CommonValidationUtils.isLessThanExponent(argument.expression(), 13)) {
111+
if (isLessThan(argument.expression(), (int) Math.pow(2, 13)) || isLessThanExponent(argument.expression(), 13)) {
108112
ctx.addIssue(argument, SCRYPT_PARAMETERS_MESSAGE);
109113
}
110114
});
@@ -141,6 +145,30 @@ public class FastHashingOrPlainTextCheck extends PythonSubscriptionCheck {
141145
2, ROUNDS, PBKDF2_MESSAGE
142146
);
143147

148+
private static final CallValidator BCRYPT_GENSALT = new ArgumentValidator(
149+
0, ROUNDS, (ctx, argument) -> {
150+
if (isLessThan(argument.expression(), 12)) {
151+
ctx.addIssue(argument, BCRYPT_MESSAGE);
152+
}
153+
});
154+
private static final CallValidator BCRYPT_KDF = new ArgumentValidator(
155+
3, ROUNDS, (ctx, argument) -> {
156+
if (isLessThan(argument.expression(), 4096) || isLessThanExponent(argument.expression(), 12)) {
157+
ctx.addIssue(argument, BCRYPT_MESSAGE);
158+
}
159+
});
160+
private static final CallValidator PASSLIB_BCRYPT = new ArgumentValidator(
161+
3, ROUNDS, (ctx, argument) -> {
162+
if (isLessThan(argument.expression(), 12)) {
163+
ctx.addIssue(argument, BCRYPT_MESSAGE);
164+
}
165+
});
166+
private static final CallValidator FLASK_BCRYPT = new ArgumentValidator(
167+
1, ROUNDS, (ctx, argument) -> {
168+
if (isLessThan(argument.expression(), 12)) {
169+
ctx.addIssue(argument, BCRYPT_MESSAGE);
170+
}
171+
});
144172

145173
private static final Map<String, Collection<CallValidator>> CALL_EXPRESSION_VALIDATORS = Map.ofEntries(
146174
Map.entry("scrypt.hash", List.of(SCRYPT_R, SCRYPT_BUFLEN, SCRYPT_N)),
@@ -153,26 +181,65 @@ public class FastHashingOrPlainTextCheck extends PythonSubscriptionCheck {
153181
Map.entry("argon2.Parameters", List.of(new Argon2PasswordHasherValidator(4, 5, 6))),
154182
Map.entry("argon2.low_level.hash_secret", List.of(new Argon2PasswordHasherValidator(2, 3, 4))),
155183
Map.entry("argon2.low_level.hash_secret_raw", List.of(new Argon2PasswordHasherValidator(2, 3, 4))),
156-
Map.entry("passlib.handlers.argon2._Argon2Common.using", List.of(new Argon2PasswordHasherValidator(3, 4, 5)))
184+
Map.entry("passlib.handlers.argon2._Argon2Common.using", List.of(new Argon2PasswordHasherValidator(3, 4, 5))),
185+
Map.entry("bcrypt.gensalt", List.of(BCRYPT_GENSALT)),
186+
Map.entry("bcrypt.kdf", List.of(BCRYPT_KDF)),
187+
Map.entry("flask_bcrypt.generate_password_hash", List.of(FLASK_BCRYPT)),
188+
Map.entry("flask_bcrypt.Bcrypt.generate_password_hash", List.of(FLASK_BCRYPT))
157189
);
158190

159191
private static final Map<String, Collection<CallValidator>> QUALIFIED_EXPR_VALIDATOR = Map.of(
160192
"passlib.hash.pbkdf2_sha1.using", List.of(PASSLIB_PBKDF2),
161193
"passlib.hash.pbkdf2_sha256.using", List.of(PASSLIB_PBKDF2, PASSLIB_MISSING_ROUNDS),
162-
"passlib.hash.pbkdf2_sha512.using", List.of(PASSLIB_PBKDF2, PASSLIB_MISSING_ROUNDS)
194+
"passlib.hash.pbkdf2_sha512.using", List.of(PASSLIB_PBKDF2, PASSLIB_MISSING_ROUNDS),
195+
"passlib.hash.bcrypt.using", List.of(PASSLIB_BCRYPT)
163196
);
164197

165198

166199
private TypeCheckBuilder argon2CheapestProfileTypeChecker = null;
200+
private TypeCheckBuilder flaskConfigTypeChecker = null;
167201
@Override
168202
public void initialize(Context context) {
169203
context.registerSyntaxNodeConsumer(Tree.Kind.FILE_INPUT, this::registerTypeCheckers);
170204
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, FastHashingOrPlainTextCheck::checkCallExpr);
171205
context.registerSyntaxNodeConsumer(Tree.Kind.NAME, this::checkName);
206+
context.registerSyntaxNodeConsumer(Tree.Kind.ASSIGNMENT_STMT, subscriptionContext -> checkAssignment(subscriptionContext, flaskConfigTypeChecker));
172207
}
173208

174209
private void registerTypeCheckers(SubscriptionContext subscriptionContext) {
175210
argon2CheapestProfileTypeChecker = subscriptionContext.typeChecker().typeCheckBuilder().isTypeWithFqn("argon2.profiles.CHEAPEST");
211+
flaskConfigTypeChecker = subscriptionContext.typeChecker().typeCheckBuilder().isInstanceOf("flask.config.Config");
212+
}
213+
214+
private static void checkAssignment(SubscriptionContext subscriptionContext, TypeCheckBuilder flaskConfigTypeChecker) {
215+
var stmt = (AssignmentStatement) subscriptionContext.syntaxNode();
216+
var lhsSubscription = stmt.lhsExpressions().stream().findFirst()
217+
.map(ExpressionList::expressions)
218+
.flatMap(list -> list.stream().findFirst())
219+
.filter(expression -> subscriptionIsFlaskBcryptConfig(expression, flaskConfigTypeChecker));
220+
221+
if (lhsSubscription.isEmpty()) {
222+
return;
223+
}
224+
225+
if (isLessThan(stmt.assignedValue(), 12)) {
226+
subscriptionContext.addIssue(stmt.assignedValue(), BCRYPT_MESSAGE);
227+
}
228+
}
229+
230+
private static boolean subscriptionIsFlaskBcryptConfig(Expression expression, TypeCheckBuilder flaskConfigTypeChecker) {
231+
if (!expression.is(Tree.Kind.SUBSCRIPTION)) {
232+
return false;
233+
}
234+
var subscription = (SubscriptionExpression) expression;
235+
if (flaskConfigTypeChecker.check(subscription.object().typeV2()) != TriBool.TRUE) {
236+
return false;
237+
}
238+
var subscriptMatch = subscription.subscripts().expressions()
239+
.stream()
240+
.findFirst()
241+
.filter(expr -> "BCRYPT_LOG_ROUNDS".equals(singleAssignedString(expr)));
242+
return subscriptMatch.isPresent();
176243
}
177244

178245
private void checkName(SubscriptionContext subscriptionContext) {

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,3 +303,45 @@ def manual_unsafe():
303303

304304
argon2.using(time_cost=4, memory_cost=7167, parallelism=1) # Noncompliant
305305
argon2.using(time_cost=4, memory_cost=7167, parallelism=2)
306+
307+
308+
## bcrypt
309+
def bcrypt():
310+
from bcrypt import gensalt, kdf
311+
312+
gensalt(11) # Noncompliant {{Use strong bcrypt parameters.}}
313+
gensalt(12)
314+
315+
kdf(password, salt, key_bytes, 4095, ignore_few_rounds) # Noncompliant {{Use strong bcrypt parameters.}}
316+
kdf(password, salt, key_bytes, 4096, ignore_few_rounds)
317+
kdf(password, salt, key_bytes, 1 << 11, ignore_few_rounds) # Noncompliant {{Use strong bcrypt parameters.}}
318+
kdf(password, salt, key_bytes, 1 << 12, ignore_few_rounds)
319+
320+
321+
def passlib():
322+
from passlib.hash import bcrypt
323+
324+
bcrypt.using(rounds=11) # Noncompliant {{Use strong bcrypt parameters.}}
325+
bcrypt.using(rounds=12)
326+
327+
328+
def flask():
329+
from flask_bcrypt import generate_password_hash
330+
generate_password_hash(password, 11) # Noncompliant {{Use strong bcrypt parameters.}}
331+
generate_password_hash(password, 12)
332+
333+
from flask_bcrypt import Bcrypt
334+
bcrypt_app = Bcrypt(app)
335+
bcrypt_app.generate_password_hash(password, 11) # Noncompliant {{Use strong bcrypt parameters.}}
336+
bcrypt_app.generate_password_hash(password, 12)
337+
338+
339+
def flask_config():
340+
from flask import Flask
341+
342+
app = Flask(__name__)
343+
app.config['BCRYPT_LOG_ROUNDS'] = 11 # Noncompliant {{Use strong bcrypt parameters.}}
344+
app.config['BCRYPT_LOG_ROUNDS'] = 12
345+
346+
some_dict = {}
347+
some_dict['BCRYPT_LOG_ROUNDS'] = 11

0 commit comments

Comments
 (0)