Skip to content

Commit 39d8589

Browse files
committed
Python: Add basic taint modeling of tornado request
1 parent 4641150 commit 39d8589

File tree

2 files changed

+162
-20
lines changed

2 files changed

+162
-20
lines changed

python/ql/src/semmle/python/frameworks/Tornado.qll

Lines changed: 143 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ private module Tornado {
3333
* WARNING: Only holds for a few predefined attributes.
3434
*/
3535
private DataFlow::Node tornado_attr(DataFlow::TypeTracker t, string attr_name) {
36-
attr_name in ["web"] and
36+
attr_name in ["web", "httputil"] and
3737
(
3838
t.start() and
3939
result = DataFlow::importNode("tornado" + "." + attr_name)
@@ -221,6 +221,148 @@ private module Tornado {
221221
)
222222
}
223223
}
224+
225+
private class RequestAttrAccess extends tornado::httputil::HttpServerRequest::InstanceSource {
226+
RequestAttrAccess() {
227+
this.(DataFlow::AttrRead).getObject() = instance() and
228+
this.(DataFlow::AttrRead).getAttributeName() = "request"
229+
}
230+
}
231+
}
232+
}
233+
234+
// -------------------------------------------------------------------------
235+
// tornado.httputil
236+
// -------------------------------------------------------------------------
237+
/** Gets a reference to the `tornado.httputil` module. */
238+
DataFlow::Node httputil() { result = tornado_attr("httputil") }
239+
240+
/** Provides models for the `tornado.httputil` module */
241+
module httputil {
242+
/**
243+
* Gets a reference to the attribute `attr_name` of the `tornado.httputil` module.
244+
* WARNING: Only holds for a few predefined attributes.
245+
*/
246+
private DataFlow::Node httputil_attr(DataFlow::TypeTracker t, string attr_name) {
247+
attr_name in ["HTTPServerRequest"] and
248+
(
249+
t.start() and
250+
result = DataFlow::importNode("tornado.httputil" + "." + attr_name)
251+
or
252+
t.startInAttr(attr_name) and
253+
result = httputil()
254+
)
255+
or
256+
// Due to bad performance when using normal setup with `httputil_attr(t2, attr_name).track(t2, t)`
257+
// we have inlined that code and forced a join
258+
exists(DataFlow::TypeTracker t2 |
259+
exists(DataFlow::StepSummary summary |
260+
httputil_attr_first_join(t2, attr_name, result, summary) and
261+
t = t2.append(summary)
262+
)
263+
)
264+
}
265+
266+
pragma[nomagic]
267+
private predicate httputil_attr_first_join(
268+
DataFlow::TypeTracker t2, string attr_name, DataFlow::Node res,
269+
DataFlow::StepSummary summary
270+
) {
271+
DataFlow::StepSummary::step(httputil_attr(t2, attr_name), res, summary)
272+
}
273+
274+
/**
275+
* Gets a reference to the attribute `attr_name` of the `tornado.httputil` module.
276+
* WARNING: Only holds for a few predefined attributes.
277+
*/
278+
private DataFlow::Node httputil_attr(string attr_name) {
279+
result = httputil_attr(DataFlow::TypeTracker::end(), attr_name)
280+
}
281+
282+
/**
283+
* Provides models for the `tornado.httputil.HttpServerRequest` class
284+
*
285+
* See https://www.tornadoweb.org/en/stable/httputil.html#tornado.httputil.HTTPServerRequest.
286+
*/
287+
module HttpServerRequest {
288+
/** Gets a reference to the `tornado.httputil.HttpServerRequest` class. */
289+
private DataFlow::Node classRef(DataFlow::TypeTracker t) {
290+
t.start() and
291+
result = httputil_attr("HttpServerRequest")
292+
or
293+
exists(DataFlow::TypeTracker t2 | result = classRef(t2).track(t2, t))
294+
}
295+
296+
/** Gets a reference to the `tornado.httputil.HttpServerRequest` class. */
297+
DataFlow::Node classRef() { result = classRef(DataFlow::TypeTracker::end()) }
298+
299+
/**
300+
* A source of instances of `tornado.httputil.HttpServerRequest`, extend this class to model new instances.
301+
*
302+
* This can include instantiations of the class, return values from function
303+
* calls, or a special parameter that will be set when functions are called by an external
304+
* library.
305+
*
306+
* Use the predicate `HttpServerRequest::instance()` to get references to instances of `tornado.httputil.HttpServerRequest`.
307+
*/
308+
abstract class InstanceSource extends DataFlow::Node { }
309+
310+
/** A direct instantiation of `tornado.httputil.HttpServerRequest`. */
311+
private class ClassInstantiation extends InstanceSource, DataFlow::CfgNode {
312+
override CallNode node;
313+
314+
ClassInstantiation() { node.getFunction() = classRef().asCfgNode() }
315+
}
316+
317+
/** Gets a reference to an instance of `tornado.httputil.HttpServerRequest`. */
318+
private DataFlow::Node instance(DataFlow::TypeTracker t) {
319+
t.start() and
320+
result instanceof InstanceSource
321+
or
322+
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
323+
}
324+
325+
/** Gets a reference to an instance of `tornado.httputil.HttpServerRequest`. */
326+
DataFlow::Node instance() { result = instance(DataFlow::TypeTracker::end()) }
327+
328+
/** Gets a reference to the `full_url` method. */
329+
private DataFlow::Node full_url(DataFlow::TypeTracker t) {
330+
t.startInAttr("full_url") and
331+
result = instance()
332+
or
333+
exists(DataFlow::TypeTracker t2 | result = full_url(t2).track(t2, t))
334+
}
335+
336+
/** Gets a reference to the `full_url` method. */
337+
DataFlow::Node full_url() { result = full_url(DataFlow::TypeTracker::end()) }
338+
339+
private class AdditionalTaintStep extends TaintTracking::AdditionalTaintStep {
340+
override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
341+
// Method access
342+
nodeTo.(DataFlow::AttrRead).getObject() = nodeFrom and
343+
nodeFrom = instance() and
344+
nodeTo in [full_url()]
345+
or
346+
// Method call
347+
nodeTo.asCfgNode().(CallNode).getFunction() = nodeFrom.asCfgNode() and
348+
nodeFrom in [full_url()]
349+
or
350+
// Attributes
351+
nodeFrom = instance() and
352+
exists(DataFlow::AttrRead read | nodeTo = read and read.getObject() = nodeFrom |
353+
read.getAttributeName() in [
354+
// str / bytes
355+
"uri", "path", "query", "remote_ip", "body",
356+
// Dict[str, List[bytes]]
357+
"arguments", "query_arguments", "body_arguments",
358+
// dict-like, https://www.tornadoweb.org/en/stable/httputil.html#tornado.httputil.HTTPHeaders
359+
"headers",
360+
// Dict[str, http.cookies.Morsel]
361+
"cookies"
362+
]
363+
)
364+
}
365+
}
224366
}
225367
}
226368
}

python/ql/test/experimental/library-tests/frameworks/tornado/TestTaint.expected

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,27 @@
1515
| taint_test.py:26 | ok | get | self.path_kwargs |
1616
| taint_test.py:27 | ok | get | self.path_kwargs["name"] |
1717
| taint_test.py:34 | ok | get | request |
18-
| taint_test.py:36 | fail | get | request.uri |
19-
| taint_test.py:37 | fail | get | request.path |
20-
| taint_test.py:38 | fail | get | request.query |
21-
| taint_test.py:39 | fail | get | request.full_url() |
22-
| taint_test.py:41 | fail | get | request.remote_ip |
23-
| taint_test.py:43 | fail | get | request.body |
24-
| taint_test.py:45 | fail | get | request.arguments |
25-
| taint_test.py:46 | fail | get | request.arguments["name"] |
26-
| taint_test.py:47 | fail | get | request.arguments["name"][0] |
27-
| taint_test.py:49 | fail | get | request.query_arguments |
28-
| taint_test.py:50 | fail | get | request.query_arguments["name"] |
29-
| taint_test.py:51 | fail | get | request.query_arguments["name"][0] |
30-
| taint_test.py:53 | fail | get | request.body_arguments |
31-
| taint_test.py:54 | fail | get | request.body_arguments["name"] |
32-
| taint_test.py:55 | fail | get | request.body_arguments["name"][0] |
33-
| taint_test.py:58 | fail | get | request.headers |
34-
| taint_test.py:59 | fail | get | request.headers["header-name"] |
18+
| taint_test.py:36 | ok | get | request.uri |
19+
| taint_test.py:37 | ok | get | request.path |
20+
| taint_test.py:38 | ok | get | request.query |
21+
| taint_test.py:39 | ok | get | request.full_url() |
22+
| taint_test.py:41 | ok | get | request.remote_ip |
23+
| taint_test.py:43 | ok | get | request.body |
24+
| taint_test.py:45 | ok | get | request.arguments |
25+
| taint_test.py:46 | ok | get | request.arguments["name"] |
26+
| taint_test.py:47 | ok | get | request.arguments["name"][0] |
27+
| taint_test.py:49 | ok | get | request.query_arguments |
28+
| taint_test.py:50 | ok | get | request.query_arguments["name"] |
29+
| taint_test.py:51 | ok | get | request.query_arguments["name"][0] |
30+
| taint_test.py:53 | ok | get | request.body_arguments |
31+
| taint_test.py:54 | ok | get | request.body_arguments["name"] |
32+
| taint_test.py:55 | ok | get | request.body_arguments["name"][0] |
33+
| taint_test.py:58 | ok | get | request.headers |
34+
| taint_test.py:59 | ok | get | request.headers["header-name"] |
3535
| taint_test.py:60 | fail | get | request.headers.get_list(..) |
3636
| taint_test.py:61 | fail | get | request.headers.get_all() |
3737
| taint_test.py:62 | fail | get | ListComp |
38-
| taint_test.py:65 | fail | get | request.cookies |
39-
| taint_test.py:66 | fail | get | request.cookies["cookie-name"] |
38+
| taint_test.py:65 | ok | get | request.cookies |
39+
| taint_test.py:66 | ok | get | request.cookies["cookie-name"] |
4040
| taint_test.py:67 | fail | get | request.cookies["cookie-name"].key |
4141
| taint_test.py:68 | fail | get | request.cookies["cookie-name"].value |

0 commit comments

Comments
 (0)