Skip to content

Commit 35cba17

Browse files
committed
Python: Consider taint of client http requests
1 parent b68d280 commit 35cba17

File tree

3 files changed

+67
-12
lines changed

3 files changed

+67
-12
lines changed

python/ql/lib/semmle/python/Concepts.qll

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -822,8 +822,8 @@ module HTTP {
822822
* extend `Request::Range` instead.
823823
*/
824824
class Request extends DataFlow::Node instanceof Request::Range {
825-
/** Gets a node which returns the body of the response */
826-
DataFlow::Node getResponseBody() { result = super.getResponseBody() }
825+
/** Gets a node that provides the response to this request. */
826+
DataFlow::Node getResponse() { result = super.getResponse() }
827827

828828
/**
829829
* Gets a node that contributes to the URL of the request.
@@ -857,8 +857,8 @@ module HTTP {
857857
* extend `Request` instead.
858858
*/
859859
abstract class Range extends DataFlow::Node {
860-
/** Gets a node which returns the body of the response */
861-
abstract DataFlow::Node getResponseBody();
860+
/** Gets a node that provides the response to this request. */
861+
abstract DataFlow::Node getResponse();
862862

863863
/**
864864
* Gets a node that contributes to the URL of the request.
@@ -882,14 +882,19 @@ module HTTP {
882882
}
883883
}
884884

885-
/** The response body from an outgoing HTTP request, considered as a remote flow source */
886-
private class RequestResponseBody extends RemoteFlowSource::Range, DataFlow::Node {
887-
Request request;
888-
889-
RequestResponseBody() { this = request.getResponseBody() }
890-
891-
override string getSourceType() { result = request.getFramework() + " response body" }
885+
/**
886+
* Additional taint step from a client request with user-controlled URL to the response.
887+
*/
888+
private class HttpClientRequestAdditionalTaintStep extends TaintTracking::AdditionalTaintStep {
889+
override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
890+
exists(Request req |
891+
nodeFrom = req.getUrl() and
892+
nodeTo = req.getResponse()
893+
)
894+
}
892895
}
896+
// TODO: investigate whether we should treat responses to client requests as
897+
// remote-flow-sources in general.
893898
}
894899
}
895900

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ private module Requests {
5151
result = this.getArg(1)
5252
}
5353

54-
override DataFlow::Node getResponseBody() { none() }
54+
override DataFlow::Node getResponse() { result = this }
5555

5656
/** Gets the `verify` argument to this outgoing requests call. */
5757
DataFlow::Node getVerifyArg() { result = this.getArgByName("verify") }
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import requests
2+
3+
from flask import Flask, request
4+
5+
app = Flask(__name__)
6+
7+
8+
@app.route("/taint_test") # $ routeSetup="/taint_test"
9+
def test_taint(): # $ requestHandler
10+
url = request.args['untrusted_input']
11+
12+
# response from a request to a user-controlled URL should be considered
13+
# user-controlled as well.
14+
resp = requests.get(url) # $ clientRequestUrl=url
15+
16+
ensure_tainted(
17+
# see https://docs.python-requests.org/en/latest/api/#requests.Response
18+
resp, # $ tainted
19+
resp.text, # $ MISSING: tainted
20+
resp.content, # $ MISSING: tainted
21+
resp.json(), # $ MISSING: tainted
22+
23+
# file-like
24+
resp.raw, # $ MISSING: tainted
25+
26+
resp.links, # $ MISSING: tainted
27+
resp.links['key'], # $ MISSING: tainted
28+
resp.links.get('key'), # $ MISSING: tainted
29+
30+
resp.cookies, # $ MISSING: tainted
31+
resp.cookies['key'], # $ MISSING: tainted
32+
resp.cookies.get('key'), # $ MISSING: tainted
33+
34+
resp.headers, # $ MISSING: tainted
35+
resp.headers['key'], # $ MISSING: tainted
36+
resp.headers.get('key'), # $ MISSING: tainted
37+
)
38+
39+
for content_chunk in resp.iter_content():
40+
ensure_tainted(content_chunk) # $ MISSING: tainted
41+
42+
for line in resp.iter_lines():
43+
ensure_tainted(line) # $ MISSING: tainted
44+
45+
# for now, we don't assume that the response to ANY outgoing request is a remote
46+
# flow source, since this could lead to FPs.
47+
# TODO: investigate whether we should consider this a remote flow source.
48+
trusted_url = "https://internal-api-that-i-trust.com"
49+
resp = requests.get(trusted_url) # $ clientRequestUrl=trusted_url
50+
ensure__not_tainted(resp)

0 commit comments

Comments
 (0)