Skip to content

Commit 9dbb451

Browse files
authored
Merge pull request #9463 from RasmusWL/req-wo-cert-validation
Python: Rewrite `py/request-without-cert-validation`
2 parents 483281e + cfd640b commit 9dbb451

File tree

16 files changed

+200
-49
lines changed

16 files changed

+200
-49
lines changed

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -662,7 +662,7 @@ private module AiohttpClientModel {
662662
private API::Node instance() { result = classRef().getReturn() }
663663

664664
/** A method call on a ClientSession that sends off a request */
665-
private class OutgoingRequestCall extends HTTP::Client::Request::Range, DataFlow::CallCfgNode {
665+
private class OutgoingRequestCall extends HTTP::Client::Request::Range, API::CallNode {
666666
string methodName;
667667

668668
OutgoingRequestCall() {
@@ -685,8 +685,14 @@ private module AiohttpClientModel {
685685
override predicate disablesCertificateValidation(
686686
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
687687
) {
688-
// TODO: Look into disabling certificate validation
689-
none()
688+
exists(API::Node param | param = this.getKeywordParameter(["ssl", "verify_ssl"]) |
689+
disablingNode = param.getARhs() and
690+
argumentOrigin = param.getAValueReachingRhs() and
691+
// aiohttp.client treats `None` as the default and all other "falsey" values as `False`.
692+
argumentOrigin.asExpr().(ImmutableLiteral).booleanValue() = false and
693+
not argumentOrigin.asExpr() instanceof None
694+
)
695+
// TODO: Handling of SSLContext passed as ssl/ssl_context arguments
690696
}
691697
}
692698
}

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

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@ private import semmle.python.ApiGraphs
1818
* - https://www.python-httpx.org/
1919
*/
2020
private module HttpxModel {
21-
private class RequestCall extends HTTP::Client::Request::Range, DataFlow::CallCfgNode {
21+
/**
22+
* An outgoing HTTP request, from the `httpx` library.
23+
*
24+
* See https://www.python-httpx.org/api/
25+
*/
26+
private class RequestCall extends HTTP::Client::Request::Range, API::CallNode {
2227
string methodName;
2328

2429
RequestCall() {
@@ -39,32 +44,32 @@ private module HttpxModel {
3944
override predicate disablesCertificateValidation(
4045
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
4146
) {
42-
// TODO: Look into disabling certificate validation
43-
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
4452
}
4553
}
4654

4755
/**
4856
* Provides models for the `httpx.[Async]Client` class
4957
*
50-
* See https://www.python-httpx.org/async/
58+
* See https://www.python-httpx.org/api/#client
5159
*/
5260
module Client {
5361
/** Get a reference to the `httpx.Client` or `httpx.AsyncClient` class. */
5462
private API::Node classRef() {
5563
result = API::moduleImport("httpx").getMember(["Client", "AsyncClient"])
5664
}
5765

58-
/** Get a reference to an `httpx.Client` or `httpx.AsyncClient` instance. */
59-
private API::Node instance() { result = classRef().getReturn() }
60-
6166
/** A method call on a Client that sends off a request */
6267
private class OutgoingRequestCall extends HTTP::Client::Request::Range, DataFlow::CallCfgNode {
6368
string methodName;
6469

6570
OutgoingRequestCall() {
6671
methodName in [HTTP::httpVerbLower(), "request", "stream"] and
67-
this = instance().getMember(methodName).getACall()
72+
this = classRef().getReturn().getMember(methodName).getACall()
6873
}
6974

7075
override DataFlow::Node getAUrlPart() {
@@ -80,8 +85,16 @@ private module HttpxModel {
8085
override predicate disablesCertificateValidation(
8186
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
8287
) {
83-
// TODO: Look into disabling certificate validation
84-
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+
)
8598
}
8699
}
87100
}

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,14 @@ private import semmle.python.frameworks.Stdlib
2020
*
2121
* See
2222
* - https://pypi.org/project/requests/
23-
* - https://docs.python-requests.org/en/latest/
23+
* - https://requests.readthedocs.io/en/latest/
2424
*/
2525
private module Requests {
26+
/**
27+
* An outgoing HTTP request, from the `requests` library.
28+
*
29+
* See https://requests.readthedocs.io/en/latest/api/#requests.request
30+
*/
2631
private class OutgoingRequestCall extends HTTP::Client::Request::Range, API::CallNode {
2732
string methodName;
2833

@@ -58,6 +63,7 @@ private module Requests {
5863
) {
5964
disablingNode = this.getKeywordParameter("verify").getARhs() and
6065
argumentOrigin = this.getKeywordParameter("verify").getAValueReachingRhs() and
66+
// requests treats `None` as the default and all other "falsey" values as `False`.
6167
argumentOrigin.asExpr().(ImmutableLiteral).booleanValue() = false and
6268
not argumentOrigin.asExpr() instanceof None
6369
}
@@ -81,7 +87,7 @@ private module Requests {
8187
/**
8288
* Provides models for the `requests.models.Response` class
8389
*
84-
* See https://docs.python-requests.org/en/latest/api/#requests.Response.
90+
* See https://requests.readthedocs.io/en/latest/api/#requests.Response.
8591
*/
8692
module Response {
8793
/** Gets a reference to the `requests.models.Response` class. */

python/ql/lib/semmle/python/frameworks/Stdlib/Urllib.qll

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ private module Urllib {
4242
override predicate disablesCertificateValidation(
4343
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
4444
) {
45-
// TODO: Look into disabling certificate validation
45+
// cannot enable/disable certificate validation on this object, only when used
46+
// with `urlopen`, which is modeled below
4647
none()
4748
}
4849
}
@@ -63,7 +64,8 @@ private module Urllib {
6364
override predicate disablesCertificateValidation(
6465
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
6566
) {
66-
// TODO: Look into disabling certificate validation
67+
// will validate certificate by default, see https://github.com/python/cpython/blob/243ed5439c32e8517aa745bc2ca9774d99c99d0f/Lib/http/client.py#L1420-L1421
68+
// TODO: Handling of insecure SSLContext passed to context argument
6769
none()
6870
}
6971
}

python/ql/lib/semmle/python/frameworks/Stdlib/Urllib2.qll

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ private module Urllib2 {
3030
override predicate disablesCertificateValidation(
3131
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
3232
) {
33-
// TODO: Look into disabling certificate validation
33+
// cannot enable/disable certificate validation on this object, only when used
34+
// with `urlopen`, which is modeled below
3435
none()
3536
}
3637
}
@@ -49,7 +50,8 @@ private module Urllib2 {
4950
override predicate disablesCertificateValidation(
5051
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
5152
) {
52-
// TODO: Look into disabling certificate validation
53+
// will validate certificate by default
54+
// TODO: Handling of insecure SSLContext passed to context argument
5355
none()
5456
}
5557
}

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

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,20 +42,18 @@ private module Urllib3 {
4242
.getASubclass+()
4343
}
4444

45-
/** Gets a reference to an instance of a `urllib3.request.RequestMethods` subclass. */
46-
private API::Node instance() { result = classRef().getReturn() }
47-
4845
/**
4946
* A call to a method making an outgoing request.
5047
*
5148
* See
5249
* - https://urllib3.readthedocs.io/en/stable/reference/urllib3.request.html#urllib3.request.RequestMethods
5350
* - https://urllib3.readthedocs.io/en/stable/reference/urllib3.connectionpool.html#urllib3.HTTPConnectionPool.urlopen
5451
*/
55-
private class RequestCall extends HTTP::Client::Request::Range, DataFlow::CallCfgNode {
52+
private class RequestCall extends HTTP::Client::Request::Range, API::CallNode {
5653
RequestCall() {
5754
this =
58-
instance()
55+
classRef()
56+
.getReturn()
5957
.getMember(["request", "request_encode_url", "request_encode_body", "urlopen"])
6058
.getACall()
6159
}
@@ -67,8 +65,22 @@ private module Urllib3 {
6765
override predicate disablesCertificateValidation(
6866
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
6967
) {
70-
// TODO: Look into disabling certificate validation
71-
none()
68+
exists(API::CallNode constructor |
69+
constructor = classRef().getACall() and
70+
this = constructor.getReturn().getAMember().getACall()
71+
|
72+
// cert_reqs
73+
// see https://urllib3.readthedocs.io/en/stable/user-guide.html?highlight=cert_reqs#certificate-verification
74+
disablingNode = constructor.getKeywordParameter("cert_reqs").getARhs() and
75+
argumentOrigin = constructor.getKeywordParameter("cert_reqs").getAValueReachingRhs() and
76+
argumentOrigin.asExpr().(StrConst).getText() = "CERT_NONE"
77+
or
78+
// assert_hostname
79+
// see https://urllib3.readthedocs.io/en/stable/reference/urllib3.connectionpool.html?highlight=assert_hostname#urllib3.HTTPSConnectionPool
80+
disablingNode = constructor.getKeywordParameter("assert_hostname").getARhs() and
81+
argumentOrigin = constructor.getKeywordParameter("assert_hostname").getAValueReachingRhs() and
82+
argumentOrigin.asExpr().(BooleanLiteral).booleanValue() = false
83+
)
7284
}
7385
}
7486
}

python/ql/src/Security/CWE-295/RequestWithoutValidation.ql

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ private import semmle.python.dataflow.new.DataFlow
1515
private import semmle.python.Concepts
1616
private import semmle.python.ApiGraphs
1717

18-
from API::CallNode call, DataFlow::Node falseyOrigin, string verb
18+
from
19+
HTTP::Client::Request request, DataFlow::Node disablingNode, DataFlow::Node origin, string ending
1920
where
20-
verb = HTTP::httpVerbLower() and
21-
call = API::moduleImport("requests").getMember(verb).getACall() and
22-
falseyOrigin = call.getKeywordParameter("verify").getAValueReachingRhs() and
23-
// requests treats `None` as the default and all other "falsey" values as `False`.
24-
falseyOrigin.asExpr().(ImmutableLiteral).booleanValue() = false and
25-
not falseyOrigin.asExpr() instanceof None
26-
select call, "Call to requests." + verb + " with verify=$@", falseyOrigin, "False"
21+
request.disablesCertificateValidation(disablingNode, origin) and
22+
// Showing the origin is only useful when it's a different node than the one disabling
23+
// certificate validation, for example in `requests.get(..., verify=arg)`, `arg` would
24+
// be the `disablingNode`, and the `origin` would be the place were `arg` got its
25+
// value from.
26+
if disablingNode = origin then ending = "." else ending = " by the value from $@."
27+
select request, "This request may run without certificate validation because it is $@" + ending,
28+
disablingNode, "disabled here", origin, "here"
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: majorAnalysis
3+
---
4+
* Improved library modeling for the query "Request without certificate validation" (`py/request-without-cert-validation`), so it now also covers `httpx`, `aiohttp.client`, and `urllib3`.

python/ql/test/library-tests/frameworks/aiohttp/client_request.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import aiohttp
22
import asyncio
3+
import ssl
34

45
s = aiohttp.ClientSession()
56
resp = s.request("method", "url") # $ clientRequestUrlPart="url"
@@ -13,4 +14,22 @@
1314
s = aiohttp.ClientSession()
1415
resp = s.post("url") # $ clientRequestUrlPart="url"
1516
resp = s.patch("url") # $ clientRequestUrlPart="url"
16-
resp = s.options("url") # $ clientRequestUrlPart="url"
17+
resp = s.options("url") # $ clientRequestUrlPart="url"
18+
19+
# disabling of SSL validation
20+
# see https://docs.aiohttp.org/en/stable/client_reference.html#aiohttp.ClientSession.request
21+
s.get("url", ssl=False) # $ clientRequestUrlPart="url" clientRequestCertValidationDisabled
22+
s.get("url", ssl=0) # $ clientRequestUrlPart="url" clientRequestCertValidationDisabled
23+
# None is treated as default and so does _not_ disable the check
24+
s.get("url", ssl=None) # $ clientRequestUrlPart="url"
25+
26+
# deprecated since 3.0, but still supported
27+
s.get("url", verify_ssl=False) # $ clientRequestUrlPart="url" clientRequestCertValidationDisabled
28+
29+
# A manually constructed SSLContext does not have safe defaults, so is effectively the
30+
# same as turning off SSL validation
31+
context = ssl.SSLContext()
32+
assert context.check_hostname == False
33+
assert context.verify_mode == ssl.VerifyMode.CERT_NONE
34+
35+
s.get("url", ssl=context) # $ clientRequestUrlPart="url" MISSING: clientRequestCertValidationDisabled

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

Lines changed: 28 additions & 7 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"
@@ -15,10 +16,30 @@
1516
response = client.request("method", url="url") # $ clientRequestUrlPart="url"
1617
response = client.stream("method", url="url") # $ clientRequestUrlPart="url"
1718

18-
client = httpx.AsyncClient()
19-
response = client.get("url") # $ clientRequestUrlPart="url"
20-
response = client.post("url") # $ clientRequestUrlPart="url"
21-
response = client.patch("url") # $ clientRequestUrlPart="url"
22-
response = client.options("url") # $ clientRequestUrlPart="url"
23-
response = client.request("method", url="url") # $ clientRequestUrlPart="url"
24-
response = client.stream("method", url="url") # $ clientRequestUrlPart="url"
19+
async def async_test():
20+
client = httpx.AsyncClient()
21+
response = await client.get("url") # $ clientRequestUrlPart="url"
22+
response = await client.post("url") # $ clientRequestUrlPart="url"
23+
response = await client.patch("url") # $ clientRequestUrlPart="url"
24+
response = await client.options("url") # $ clientRequestUrlPart="url"
25+
response = await client.request("method", url="url") # $ clientRequestUrlPart="url"
26+
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)