Skip to content

Commit 227f509

Browse files
maksim-grebeniuk-sonarsourcesonartech
authored andcommitted
SONARPY-1690 [S4830] Add Requests session support (#189)
GitOrigin-RevId: 6c0adb625cab54b25beebf1c9cd190b371da5d6b
1 parent 46d7aaa commit 227f509

2 files changed

Lines changed: 95 additions & 33 deletions

File tree

python-checks/src/main/java/org/sonar/python/checks/VerifiedSslTlsCertificateCheck.java

Lines changed: 76 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,12 @@
5050
import org.sonar.plugins.python.api.tree.UnpackingExpression;
5151
import org.sonar.plugins.python.api.tree.WithItem;
5252
import org.sonar.plugins.python.api.tree.WithStatement;
53+
import org.sonar.plugins.python.api.types.v2.TriBool;
5354
import org.sonar.python.checks.utils.Expressions;
5455
import org.sonar.python.tree.RegularArgumentImpl;
5556
import org.sonar.python.tree.TreeUtils;
57+
import org.sonar.python.types.v2.TypeCheckBuilder;
58+
import org.sonar.python.types.v2.TypeCheckMap;
5659

5760
import static java.util.Optional.ofNullable;
5861

@@ -63,13 +66,27 @@ public class VerifiedSslTlsCertificateCheck extends PythonSubscriptionCheck {
6366
private static final String MESSAGE = "Enable server certificate validation on this SSL/TLS connection.";
6467
private static final String VERIFY_NONE = Fqn.ssl("VERIFY_NONE");
6568

69+
private TypeCheckBuilder requestsSessionTypeCheck;
70+
private TypeCheckMap<Set<String>> requestsVerifyArguments;
71+
6672
@Override
6773
public void initialize(Context context) {
74+
context.registerSyntaxNodeConsumer(Tree.Kind.FILE_INPUT, this::initTypeChecks);
6875
context.registerSyntaxNodeConsumer(Tree.Kind.WITH_STMT, VerifiedSslTlsCertificateCheck::verifyAioHttpWithSession);
6976
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, VerifiedSslTlsCertificateCheck::sslSetVerifyCheck);
70-
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, VerifiedSslTlsCertificateCheck::requestsCheck);
77+
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, this::requestsCheck);
7178
context.registerSyntaxNodeConsumer(Tree.Kind.REGULAR_ARGUMENT, VerifiedSslTlsCertificateCheck::standardSslCheckForRegularArgument);
7279
context.registerSyntaxNodeConsumer(Tree.Kind.ASSIGNMENT_STMT, VerifiedSslTlsCertificateCheck::standardSslCheckForAssignmentStatement);
80+
context.registerSyntaxNodeConsumer(Tree.Kind.ASSIGNMENT_STMT, this::requestsSessionAssignmentCheck);
81+
}
82+
83+
private void initTypeChecks(SubscriptionContext ctx) {
84+
requestsSessionTypeCheck = ctx.typeChecker().typeCheckBuilder().isInstanceOf(REQUESTS_SESSIONS_FQN);
85+
requestsVerifyArguments = new TypeCheckMap<>();
86+
CALLS_WHERE_TO_ENFORCE_TRUE_ARGUMENT.forEach(fqn -> {
87+
var check = ctx.typeChecker().typeCheckBuilder().isTypeWithFqn(fqn);
88+
requestsVerifyArguments.put(check, VERIFY_ARG_NAME);
89+
});
7390
}
7491

7592
private static void verifyAioHttpWithSession(SubscriptionContext ctx) {
@@ -104,12 +121,14 @@ private static void verifyAioHttpSessionSymbolUsages(SubscriptionContext ctx, Sy
104121
.forEach(sessionCallExpr -> verifyVulnerableMethods(ctx, sessionCallExpr, VERIFY_SSL_ARG_NAMES));
105122
}
106123

107-
/** Fully qualified name of the <code>set_verify</code> used in <code>sslSetVerifyCheck</code>. */
124+
/**
125+
* Fully qualified name of the <code>set_verify</code> used in <code>sslSetVerifyCheck</code>.
126+
*/
108127
private static final String SET_VERIFY = Fqn.context("set_verify");
109128

110129
/**
111130
* Check for the <code>OpenSSL.SSL.Context.set_verify</code> flag settings.
112-
*
131+
* <p>
113132
* Searches for `set_verify` invocations on instances of `OpenSSL.SSL.Context`,
114133
* extracts the flags from the first argument, checks that the combination of flags is secure.
115134
*
@@ -136,7 +155,9 @@ private static void sslSetVerifyCheck(SubscriptionContext subscriptionContext) {
136155
}
137156
}
138157

139-
/** Helper methods for generating FQNs frequently used in this check. */
158+
/**
159+
* Helper methods for generating FQNs frequently used in this check.
160+
*/
140161
private static class Fqn {
141162
private static String context(@SuppressWarnings("SameParameterValue") String method) {
142163
return ssl("Context." + method);
@@ -186,22 +207,19 @@ private static Optional<IssueReport> checkFlagSettings(Set<QualifiedExpression>
186207
return Optional.empty();
187208
}
188209

189-
/** Message and a token closest to the problematic position. Glorified <code>Pair&lt;A,B&gt;</code>. */
190-
private static class IssueReport {
191-
final String message;
192-
final Token token;
193-
194-
private IssueReport(String message, Token token) {
195-
this.message = message;
196-
this.token = token;
197-
}
210+
/**
211+
* Message and a token closest to the problematic position. Glorified <code>Pair&lt;A,B&gt;</code>.
212+
*/
213+
private record IssueReport(String message, Token token) {
198214
}
199215

200216
public static final Set<String> VERIFY_ARG_NAME = Set.of("verify");
217+
public static final String VERIFY_FIELD_NAME = "verify";
201218
public static final Set<String> VERIFY_SSL_ARG_NAMES = Set.of("verify_ssl", "ssl");
202219
/**
203220
* Set of FQNs of methods in <code>requests</code>-module that have the vulnerable <code>verify</code>-option.
204221
*/
222+
public static final String REQUESTS_SESSIONS_FQN = "requests.sessions.Session";
205223
private static final Set<String> CALLS_WHERE_TO_ENFORCE_TRUE_ARGUMENT = Set.of(
206224
"requests.api.request",
207225
"requests.api.get",
@@ -211,6 +229,13 @@ private IssueReport(String message, Token token) {
211229
"requests.api.delete",
212230
"requests.api.patch",
213231
"requests.api.options",
232+
REQUESTS_SESSIONS_FQN + ".request",
233+
REQUESTS_SESSIONS_FQN + ".get",
234+
REQUESTS_SESSIONS_FQN + ".head",
235+
REQUESTS_SESSIONS_FQN + ".post",
236+
REQUESTS_SESSIONS_FQN + ".put",
237+
REQUESTS_SESSIONS_FQN + ".delete",
238+
REQUESTS_SESSIONS_FQN + ".patch",
214239
"httpx.request",
215240
"httpx.stream",
216241
"httpx.get",
@@ -223,18 +248,25 @@ private IssueReport(String message, Token token) {
223248
"httpx.Client",
224249
"httpx.AsyncClient");
225250

226-
private static void requestsCheck(SubscriptionContext subscriptionContext) {
227-
var callExpr = (CallExpression) subscriptionContext.syntaxNode();
228-
var isVulnerableMethod = ofNullable(callExpr.calleeSymbol())
229-
.map(Symbol::fullyQualifiedName)
230-
.filter(CALLS_WHERE_TO_ENFORCE_TRUE_ARGUMENT::contains)
231-
.isPresent();
232-
233-
if (isVulnerableMethod) {
234-
verifyVulnerableMethods(subscriptionContext, callExpr, VERIFY_ARG_NAME);
251+
private void requestsSessionAssignmentCheck(SubscriptionContext ctx) {
252+
var assignment = (AssignmentStatement) ctx.syntaxNode();
253+
// SONARPY-2268
254+
if (assignment.lhsExpressions().get(0).expressions().size() == 1
255+
&& assignment.lhsExpressions().get(0).expressions().get(0) instanceof QualifiedExpression qualifiedExpression
256+
&& requestsSessionTypeCheck.check(qualifiedExpression.qualifier().typeV2()) == TriBool.TRUE
257+
&& VERIFY_FIELD_NAME.equals(qualifiedExpression.name().name())
258+
&& Expressions.isFalsy(assignment.assignedValue())
259+
) {
260+
ctx.addIssue(assignment, MESSAGE);
235261
}
236262
}
237263

264+
private void requestsCheck(SubscriptionContext subscriptionContext) {
265+
var callExpr = (CallExpression) subscriptionContext.syntaxNode();
266+
requestsVerifyArguments.getOptionalForType(callExpr.callee().typeV2())
267+
.ifPresent(args -> verifyVulnerableMethods(subscriptionContext, callExpr, args));
268+
}
269+
238270
private static void verifyVulnerableMethods(SubscriptionContext ctx, CallExpression callExpr, Set<String> argumentNames) {
239271
var verifyRhs = searchVerifyAssignment(callExpr, argumentNames)
240272
.or(() -> searchVerifyInKwargs(callExpr, argumentNames));
@@ -278,7 +310,7 @@ private static Optional<List<Expression>> searchVerifyAssignment(CallExpression
278310
/**
279311
* Attempts to find the <code>rhs</code> in some definition <code>kwargs = { 'verify': rhs }</code>
280312
* of <code>kwargs</code> used in the arguments of the given <code>callExpression</code>.
281-
*
313+
* <p>
282314
* Returns list of problematic expressions in the reverse order of importance (the <code>kwargs</code>-argument comes
283315
* first, the setting in the dictionary comes last).
284316
*/
@@ -292,13 +324,17 @@ private static Optional<List<Expression>> searchVerifyInKwargs(CallExpression ca
292324
.map(arg -> ((UnpackingExpression) arg).expression())
293325
.filter(Name.class::isInstance)
294326
.findFirst()
295-
.flatMap(name -> Optional.ofNullable(Expressions.singleAssignedValue((Name) name))
327+
.map(Name.class::cast)
328+
.flatMap(name -> Optional.ofNullable(Expressions.singleAssignedValue(name))
296329
.filter(DictionaryLiteral.class::isInstance)
297-
.flatMap(dict -> searchDangerousVerifySettingInDictionary((DictionaryLiteral) dict, argumentNames)
298-
.map(settingInDict -> Arrays.asList(name, settingInDict))));
330+
.map(DictionaryLiteral.class::cast)
331+
.flatMap(dict -> searchDangerousVerifySettingInDictionary(dict, argumentNames))
332+
.map(settingInDict -> Arrays.asList(name, settingInDict)));
299333
}
300334

301-
/** Searches for a dangerous falsy <code>verify: False</code> in a dictionary literal. */
335+
/**
336+
* Searches for a dangerous falsy <code>verify: False</code> in a dictionary literal.
337+
*/
302338
private static Optional<Expression> searchDangerousVerifySettingInDictionary(DictionaryLiteral dict, Set<String> argumentNames) {
303339
return dict.elements().stream()
304340
.filter(KeyValuePair.class::isInstance)
@@ -327,11 +363,15 @@ private static boolean isFalsyCollection(Expression expr) {
327363
return false;
328364
}
329365

330-
/** FQNs of collection constructors that yield a falsy collection if invoked without arguments. */
366+
/**
367+
* FQNs of collection constructors that yield a falsy collection if invoked without arguments.
368+
*/
331369
private static final Set<String> NO_ARG_FALSY_COLLECTION_CONSTRUCTORS = new HashSet<>(Arrays.asList(
332370
"set", "list", "dict"));
333371

334-
/** Detects expressions like <code>dict()</code> or <code>list()</code>. */
372+
/**
373+
* Detects expressions like <code>dict()</code> or <code>list()</code>.
374+
*/
335375
private static boolean isFalsyNoArgCollectionConstruction(CallExpression callExpr, String fqn) {
336376
return NO_ARG_FALSY_COLLECTION_CONSTRUCTORS.contains(fqn) && callExpr.arguments().isEmpty();
337377
}
@@ -378,7 +418,9 @@ private static void standardSslCheckForRegularArgument(SubscriptionContext subsc
378418
.ifPresent(vulnTok -> subscriptionContext.addIssue(vulnTok.token, MESSAGE));
379419
}
380420

381-
/** Finds the next higher line where a binding usage occurs. */
421+
/**
422+
* Finds the next higher line where a binding usage occurs.
423+
*/
382424
private static int findNextAssignmentLine(List<Usage> usages, int firstAssignmentLine) {
383425
int closestHigher = Integer.MAX_VALUE;
384426
for (Usage u : usages) {
@@ -394,7 +436,7 @@ private static int findNextAssignmentLine(List<Usage> usages, int firstAssignmen
394436

395437
/**
396438
* Selects all non-binding usages between first assignment and next assignment.
397-
*
439+
* <p>
398440
* We assume that in a vast majority of cases, there will be no complex control flow between the instantiation
399441
* of the context and the modification of the settings, thus selecting and sorting usages by line numbers
400442
* should suffice here.
@@ -419,7 +461,9 @@ private static List<Usage> selectRelevantModifyingUsages(List<Usage> usages, int
419461
"ssl.create_default_context", false,
420462
"ssl._create_default_https_context", false);
421463

422-
/** Pair and a mutable cell for combining all updates to <code>verify_mode</code>. */
464+
/**
465+
* Pair and a mutable cell for combining all updates to <code>verify_mode</code>.
466+
*/
423467
private static class VulnerabilityAndProblematicToken {
424468
boolean isInvisibleDefaultPreset;
425469
boolean isVulnerable;

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ def pyopensslTest():
2626
ctxC1.set_verify(**kwargs)
2727
ctxC1.set_verify(noSSL.THIS_DOESNT_EXIST)
2828

29-
3029
def requestsTests():
3130
# Mutably borrowed from here:
3231
# https://github.com/SonarSource/security-expected-issues/blob/master/python/rules/vulnerabilities/\
@@ -107,6 +106,25 @@ def requestsTests():
107106
requests.request('GET', 'https://example.domain', verify=range("not numeric"))
108107
requests.request('GET', 'https://example.domain', verify=set(42))
109108

109+
def requestsSessionTest():
110+
import requests
111+
112+
s1 = requests.Session()
113+
s1.verify = False # Noncompliant
114+
115+
s2 = requests.Session()
116+
s2.verify = True
117+
118+
s3 = requests.Session()
119+
s3.something_else = False
120+
121+
s4 = requests.Session()
122+
s4.request("GET", "<https://expired.badssl.com>", verify=False) # Noncompliant {{Enable server certificate validation on this SSL/TLS connection.}}
123+
# ^^^^^
124+
s4.get('https://example.domain', verify=False) # Noncompliant {{Enable server certificate validation on this SSL/TLS connection.}}
125+
# ^^^^^
126+
s4.post('https://example.domain', verify=False) # Noncompliant {{Enable server certificate validation on this SSL/TLS connection.}}
127+
# ^^^^^
110128

111129
def urllibTests():
112130
# Mutably borrowed from

0 commit comments

Comments
 (0)