Skip to content

Commit 049e872

Browse files
committed
Python: Model certificate disabling in httpx
1 parent 1a2a423 commit 049e872

File tree

2 files changed

+37
-9
lines changed
  • python/ql

2 files changed

+37
-9
lines changed

python/ql/lib/semmle/python/frameworks/Httpx.qll

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ private module HttpxModel {
2323
*
2424
* See https://www.python-httpx.org/api/
2525
*/
26-
private class RequestCall extends HTTP::Client::Request::Range, DataFlow::CallCfgNode {
26+
private class RequestCall extends HTTP::Client::Request::Range, API::CallNode {
2727
string methodName;
2828

2929
RequestCall() {
@@ -44,8 +44,11 @@ private module HttpxModel {
4444
override predicate disablesCertificateValidation(
4545
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
4646
) {
47-
// TODO: Look into disabling certificate validation
48-
none()
47+
disablingNode = this.getKeywordParameter("verify").getARhs() and
48+
argumentOrigin = this.getKeywordParameter("verify").getAValueReachingRhs() and
49+
// unlike `requests`, httpx treats `None` as turning off verify (and not as the default)
50+
argumentOrigin.asExpr().(ImmutableLiteral).booleanValue() = false
51+
// TODO: Handling of insecure SSLContext passed to verify argument
4952
}
5053
}
5154

@@ -60,16 +63,13 @@ private module HttpxModel {
6063
result = API::moduleImport("httpx").getMember(["Client", "AsyncClient"])
6164
}
6265

63-
/** Get a reference to an `httpx.Client` or `httpx.AsyncClient` instance. */
64-
private API::Node instance() { result = classRef().getReturn() }
65-
6666
/** A method call on a Client that sends off a request */
6767
private class OutgoingRequestCall extends HTTP::Client::Request::Range, DataFlow::CallCfgNode {
6868
string methodName;
6969

7070
OutgoingRequestCall() {
7171
methodName in [HTTP::httpVerbLower(), "request", "stream"] and
72-
this = instance().getMember(methodName).getACall()
72+
this = classRef().getReturn().getMember(methodName).getACall()
7373
}
7474

7575
override DataFlow::Node getAUrlPart() {
@@ -85,8 +85,16 @@ private module HttpxModel {
8585
override predicate disablesCertificateValidation(
8686
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
8787
) {
88-
// TODO: Look into disabling certificate validation
89-
none()
88+
exists(API::CallNode constructor |
89+
constructor = classRef().getACall() and
90+
this = constructor.getReturn().getMember(methodName).getACall()
91+
|
92+
disablingNode = constructor.getKeywordParameter("verify").getARhs() and
93+
argumentOrigin = constructor.getKeywordParameter("verify").getAValueReachingRhs() and
94+
// unlike `requests`, httpx treats `None` as turning off verify (and not as the default)
95+
argumentOrigin.asExpr().(ImmutableLiteral).booleanValue() = false
96+
// TODO: Handling of insecure SSLContext passed to verify argument
97+
)
9098
}
9199
}
92100
}

python/ql/test/library-tests/frameworks/httpx/test.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import httpx
2+
import ssl
23

34
httpx.get("url") # $ clientRequestUrlPart="url"
45
httpx.post("url") # $ clientRequestUrlPart="url"
@@ -23,3 +24,22 @@ async def async_test():
2324
response = await client.options("url") # $ clientRequestUrlPart="url"
2425
response = await client.request("method", url="url") # $ clientRequestUrlPart="url"
2526
response = await client.stream("method", url="url") # $ clientRequestUrlPart="url"
27+
28+
# ==============================================================================
29+
# Disabling certificate validation
30+
# ==============================================================================
31+
32+
httpx.get("url", verify=False) # $ clientRequestUrlPart="url" clientRequestCertValidationDisabled
33+
httpx.get("url", verify=0) # $ clientRequestUrlPart="url" clientRequestCertValidationDisabled
34+
httpx.get("url", verify=None) # $ clientRequestUrlPart="url" clientRequestCertValidationDisabled
35+
36+
# A manually constructed SSLContext does not have safe defaults, so is effectively the
37+
# same as turning off SSL validation
38+
context = ssl.SSLContext()
39+
assert context.check_hostname == False
40+
assert context.verify_mode == ssl.VerifyMode.CERT_NONE
41+
42+
httpx.get("url", verify=context) # $ clientRequestUrlPart="url" MISSING: clientRequestCertValidationDisabled
43+
44+
client = httpx.Client(verify=False)
45+
client.get("url") # $ clientRequestUrlPart="url" clientRequestCertValidationDisabled

0 commit comments

Comments
 (0)