Skip to content

Commit cf2ee06

Browse files
committed
Python: Model requests Responses
1 parent 35cba17 commit cf2ee06

File tree

2 files changed

+95
-15
lines changed

2 files changed

+95
-15
lines changed

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

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ private import python
1010
private import semmle.python.Concepts
1111
private import semmle.python.ApiGraphs
1212
private import semmle.python.dataflow.new.DataFlow
13+
private import semmle.python.frameworks.internal.InstanceTaintStepsHelper
14+
private import semmle.python.frameworks.Stdlib
1315

1416
/**
1517
* INTERNAL: Do not use.
@@ -83,4 +85,78 @@ private module Requests {
8385
private DataFlow::LocalSourceNode verifyArgBacktracker(DataFlow::Node arg) {
8486
result = verifyArgBacktracker(DataFlow::TypeBackTracker::end(), arg)
8587
}
88+
89+
// ---------------------------------------------------------------------------
90+
// Response
91+
// ---------------------------------------------------------------------------
92+
/**
93+
* Provides models for the `requests.models.Response` class
94+
*
95+
* See https://docs.python-requests.org/en/latest/api/#requests.Response.
96+
*/
97+
module Response {
98+
/** Gets a reference to the `requests.models.Response` class. */
99+
private API::Node classRef() {
100+
result = API::moduleImport("requests").getMember("models").getMember("Response")
101+
or
102+
result = API::moduleImport("requests").getMember("Response")
103+
}
104+
105+
/**
106+
* A source of instances of `requests.models.Response`, extend this class to model new instances.
107+
*
108+
* This can include instantiations of the class, return values from function
109+
* calls, or a special parameter that will be set when functions are called by an external
110+
* library.
111+
*
112+
* Use the predicate `Response::instance()` to get references to instances of `requests.models.Response`.
113+
*/
114+
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
115+
116+
/** A direct instantiation of `requests.models.Response`. */
117+
private class ClassInstantiation extends InstanceSource, DataFlow::CallCfgNode {
118+
ClassInstantiation() { this = classRef().getACall() }
119+
}
120+
121+
/** Return value from making a reuqest. */
122+
private class RequestReturnValue extends InstanceSource, DataFlow::Node {
123+
RequestReturnValue() { this = any(OutgoingRequestCall c).getResponse() }
124+
}
125+
126+
/** Gets a reference to an instance of `requests.models.Response`. */
127+
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
128+
t.start() and
129+
result instanceof InstanceSource
130+
or
131+
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
132+
}
133+
134+
/** Gets a reference to an instance of `requests.models.Response`. */
135+
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
136+
137+
/**
138+
* Taint propagation for `requests.models.Response`.
139+
*/
140+
private class InstanceTaintSteps extends InstanceTaintStepsHelper {
141+
InstanceTaintSteps() { this = "requests.models.Response" }
142+
143+
override DataFlow::Node getInstance() { result = instance() }
144+
145+
override string getAttributeName() {
146+
result in ["text", "content", "raw", "links", "cookies", "headers"]
147+
}
148+
149+
override string getMethodName() { result in ["json", "iter_content", "iter_lines"] }
150+
151+
override string getAsyncMethodName() { none() }
152+
}
153+
154+
/** An attribute read that is a file-like instance. */
155+
private class FileLikeInstances extends Stdlib::FileLikeObject::InstanceSource {
156+
FileLikeInstances() {
157+
this.(DataFlow::AttrRead).getObject() = instance() and
158+
this.(DataFlow::AttrRead).getAttributeName() = "raw"
159+
}
160+
}
161+
}
86162
}

python/ql/test/library-tests/frameworks/requests/taint_test.py

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,34 +13,38 @@ def test_taint(): # $ requestHandler
1313
# user-controlled as well.
1414
resp = requests.get(url) # $ clientRequestUrl=url
1515

16+
requests.Response
17+
requests.models.Response
18+
1619
ensure_tainted(
1720
# see https://docs.python-requests.org/en/latest/api/#requests.Response
1821
resp, # $ tainted
19-
resp.text, # $ MISSING: tainted
20-
resp.content, # $ MISSING: tainted
21-
resp.json(), # $ MISSING: tainted
22+
resp.text, # $ tainted
23+
resp.content, # $ tainted
24+
resp.json(), # $ tainted
2225

2326
# file-like
24-
resp.raw, # $ MISSING: tainted
27+
resp.raw, # $ tainted
28+
resp.raw.read(), # $ tainted
2529

26-
resp.links, # $ MISSING: tainted
27-
resp.links['key'], # $ MISSING: tainted
28-
resp.links.get('key'), # $ MISSING: tainted
30+
resp.links, # $ tainted
31+
resp.links['key'], # $ tainted
32+
resp.links.get('key'), # $ tainted
2933

30-
resp.cookies, # $ MISSING: tainted
31-
resp.cookies['key'], # $ MISSING: tainted
32-
resp.cookies.get('key'), # $ MISSING: tainted
34+
resp.cookies, # $ tainted
35+
resp.cookies['key'], # $ tainted
36+
resp.cookies.get('key'), # $ tainted
3337

34-
resp.headers, # $ MISSING: tainted
35-
resp.headers['key'], # $ MISSING: tainted
36-
resp.headers.get('key'), # $ MISSING: tainted
38+
resp.headers, # $ tainted
39+
resp.headers['key'], # $ tainted
40+
resp.headers.get('key'), # $ tainted
3741
)
3842

3943
for content_chunk in resp.iter_content():
40-
ensure_tainted(content_chunk) # $ MISSING: tainted
44+
ensure_tainted(content_chunk) # $ tainted
4145

4246
for line in resp.iter_lines():
43-
ensure_tainted(line) # $ MISSING: tainted
47+
ensure_tainted(line) # $ tainted
4448

4549
# for now, we don't assume that the response to ANY outgoing request is a remote
4650
# flow source, since this could lead to FPs.

0 commit comments

Comments
 (0)