Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 6658ee9

Browse files
Rasmus Lerchedahl PetersenRasmus Lerchedahl Petersen
authored andcommitted
Merge branch 'python-port-reflected-xss' of https://github.com/RasmusWL/codeql into RasmusWL-python-port-reflected-xss
2 parents 64dcfbd + d295c64 commit 6658ee9

12 files changed

Lines changed: 544 additions & 34 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* @name Reflected server-side cross-site scripting
3+
* @description Writing user input directly to a web page
4+
* allows for a cross-site scripting vulnerability.
5+
* @kind path-problem
6+
* @problem.severity error
7+
* @sub-severity high
8+
* @precision high
9+
* @id py/reflective-xss
10+
* @tags security
11+
* external/cwe/cwe-079
12+
* external/cwe/cwe-116
13+
*/
14+
15+
import python
16+
import experimental.dataflow.DataFlow
17+
import experimental.dataflow.TaintTracking
18+
import experimental.semmle.python.Concepts
19+
import experimental.dataflow.RemoteFlowSources
20+
import DataFlow::PathGraph
21+
22+
class ReflectedXssConfiguration extends TaintTracking::Configuration {
23+
ReflectedXssConfiguration() { this = "ReflectedXssConfiguration" }
24+
25+
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
26+
27+
override predicate isSink(DataFlow::Node sink) {
28+
exists(HTTP::Server::HttpResponse response |
29+
response.getMimetype().toLowerCase() = "text/html" and
30+
sink = response.getBody()
31+
)
32+
}
33+
}
34+
35+
from ReflectedXssConfiguration config, DataFlow::PathNode source, DataFlow::PathNode sink
36+
where config.hasFlowPath(source, sink)
37+
select sink.getNode(), source, sink, "Cross-site scripting vulnerability due to $@.",
38+
source.getNode(), "a user-provided value"

python/ql/src/experimental/semmle/python/Concepts.qll

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ module HTTP {
160160
/** Provides classes for modeling HTTP servers. */
161161
module Server {
162162
/**
163-
* An data-flow node that sets up a route on a server.
163+
* A data-flow node that sets up a route on a server.
164164
*
165165
* Extend this class to refine existing API models. If you want to model new APIs,
166166
* extend `RouteSetup::Range` instead.
@@ -186,7 +186,7 @@ module HTTP {
186186
/** Provides a class for modeling new HTTP routing APIs. */
187187
module RouteSetup {
188188
/**
189-
* An data-flow node that sets up a route on a server.
189+
* A data-flow node that sets up a route on a server.
190190
*
191191
* Extend this class to model new APIs. If you want to refine existing API models,
192192
* extend `RouteSetup` instead.
@@ -219,5 +219,60 @@ module HTTP {
219219

220220
override string getSourceType() { result = "RoutedParameter" }
221221
}
222+
223+
/**
224+
* A data-flow node that creates a HTTP response on a server.
225+
*
226+
* Note: we don't require that this response must be sent to a client (a kind of
227+
* "if a tree falls in a forest and nobody hears it" situation).
228+
*
229+
* Extend this class to refine existing API models. If you want to model new APIs,
230+
* extend `HttpResponse::Range` instead.
231+
*/
232+
class HttpResponse extends DataFlow::Node {
233+
HttpResponse::Range range;
234+
235+
HttpResponse() { this = range }
236+
237+
/** Gets the data-flow node that specifies the body of this HTTP response. */
238+
DataFlow::Node getBody() { result = range.getBody() }
239+
240+
/** Gets the mimetype of this HTTP response, if it can be statically determined. */
241+
string getMimetype() { result = range.getMimetype() }
242+
}
243+
244+
/** Provides a class for modeling new HTTP response APIs. */
245+
module HttpResponse {
246+
/**
247+
* A data-flow node that creates a HTTP response on a server.
248+
*
249+
* Note: we don't require that this response must be sent to a client (a kind of
250+
* "if a tree falls in a forest and nobody hears it" situation).
251+
*
252+
* Extend this class to model new APIs. If you want to refine existing API models,
253+
* extend `HttpResponse` instead.
254+
*/
255+
abstract class Range extends DataFlow::Node {
256+
/** Gets the data-flow node that specifies the body of this HTTP response. */
257+
abstract DataFlow::Node getBody();
258+
259+
/** Gets the data-flow node that specifies the content-type/mimetype of this HTTP response, if any. */
260+
abstract DataFlow::Node getMimetypeOrContentTypeArg();
261+
262+
/** Gets the default mimetype that should be used if `getMimetypeOrContentTypeArg` has no results. */
263+
abstract string getMimetypeDefault();
264+
265+
/** Gets the mimetype of this HTTP response, if it can be statically determined. */
266+
string getMimetype() {
267+
exists(StrConst str |
268+
DataFlow::localFlow(DataFlow::exprNode(str), this.getMimetypeOrContentTypeArg()) and
269+
result = str.getText().splitAt(";", 0)
270+
)
271+
or
272+
not exists(this.getMimetypeOrContentTypeArg()) and
273+
result = this.getMimetypeDefault()
274+
}
275+
}
276+
}
222277
}
223278
}

python/ql/src/experimental/semmle/python/frameworks/Flask.qll

Lines changed: 177 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ private import experimental.semmle.python.frameworks.Werkzeug
1515
* See https://flask.palletsprojects.com/en/1.1.x/.
1616
*/
1717
private module FlaskModel {
18+
// ---------------------------------------------------------------------------
19+
// flask
20+
// ---------------------------------------------------------------------------
1821
/** Gets a reference to the `flask` module. */
1922
private DataFlow::Node flask(DataFlow::TypeTracker t) {
2023
t.start() and
@@ -26,21 +29,52 @@ private module FlaskModel {
2629
/** Gets a reference to the `flask` module. */
2730
DataFlow::Node flask() { result = flask(DataFlow::TypeTracker::end()) }
2831

29-
/** Provides models for the `flask` module. */
30-
module flask {
31-
/** Gets a reference to the `flask.request` object. */
32-
private DataFlow::Node request(DataFlow::TypeTracker t) {
32+
/**
33+
* Gets a reference to the attribute `attr_name` of the `flask` module.
34+
* WARNING: Only holds for a few predefined attributes.
35+
*/
36+
private DataFlow::Node flask_attr(DataFlow::TypeTracker t, string attr_name) {
37+
attr_name in ["request", "make_response", "Response"] and
38+
(
3339
t.start() and
34-
result = DataFlow::importNode("flask.request")
40+
result = DataFlow::importNode("flask" + "." + attr_name)
3541
or
36-
t.startInAttr("request") and
42+
t.startInAttr(attr_name) and
3743
result = flask()
38-
or
39-
exists(DataFlow::TypeTracker t2 | result = request(t2).track(t2, t))
40-
}
44+
)
45+
or
46+
// Due to bad performance when using normal setup with `flask_attr(t2, attr_name).track(t2, t)`
47+
// we have inlined that code and forced a join
48+
exists(DataFlow::TypeTracker t2 |
49+
exists(DataFlow::StepSummary summary |
50+
flask_attr_first_join(t2, attr_name, result, summary) and
51+
t = t2.append(summary)
52+
)
53+
)
54+
}
55+
56+
pragma[nomagic]
57+
private predicate flask_attr_first_join(
58+
DataFlow::TypeTracker t2, string attr_name, DataFlow::Node res, DataFlow::StepSummary summary
59+
) {
60+
DataFlow::StepSummary::step(flask_attr(t2, attr_name), res, summary)
61+
}
62+
63+
/**
64+
* Gets a reference to the attribute `attr_name` of the `flask` module.
65+
* WARNING: Only holds for a few predefined attributes.
66+
*/
67+
private DataFlow::Node flask_attr(string attr_name) {
68+
result = flask_attr(DataFlow::TypeTracker::end(), attr_name)
69+
}
4170

71+
/** Provides models for the `flask` module. */
72+
module flask {
4273
/** Gets a reference to the `flask.request` object. */
43-
DataFlow::Node request() { result = request(DataFlow::TypeTracker::end()) }
74+
DataFlow::Node request() { result = flask_attr("request") }
75+
76+
/** Gets a reference to the `flask.make_response` function. */
77+
DataFlow::Node make_response() { result = flask_attr("make_response") }
4478

4579
/**
4680
* Provides models for the `flask.Flask` class
@@ -96,7 +130,7 @@ private module FlaskModel {
96130
* WARNING: Only holds for a few predefined attributes.
97131
*/
98132
private DataFlow::Node instance_attr(DataFlow::TypeTracker t, string attr_name) {
99-
attr_name in ["route", "add_url_rule"] and
133+
attr_name in ["route", "add_url_rule", "make_response"] and
100134
t.startInAttr(attr_name) and
101135
result = flask::Flask::instance()
102136
or
@@ -131,9 +165,99 @@ private module FlaskModel {
131165

132166
/** Gets a reference to the `add_url_rule` method on an instance of `flask.Flask`. */
133167
DataFlow::Node add_url_rule() { result = instance_attr("add_url_rule") }
168+
169+
/** Gets a reference to the `make_response` method on an instance of `flask.Flask`. */
170+
// HACK: We can't call this predicate `make_response` since shadowing is
171+
// completely disallowed in QL. I added an underscore to move thing forwards for
172+
// now :(
173+
DataFlow::Node make_response_() { result = instance_attr("make_response") }
174+
175+
/** Gets a reference to the `response_class` attribute on the `flask.Flask` class or an instance. */
176+
private DataFlow::Node response_class(DataFlow::TypeTracker t) {
177+
t.startInAttr("response_class") and
178+
result in [classRef(), instance()]
179+
or
180+
exists(DataFlow::TypeTracker t2 | result = response_class(t2).track(t2, t))
181+
}
182+
183+
/**
184+
* Gets a reference to the `response_class` attribute on the `flask.Flask` class or an instance.
185+
*
186+
* See https://flask.palletsprojects.com/en/1.1.x/api/#flask.Flask.response_class
187+
*/
188+
DataFlow::Node response_class() { result = response_class(DataFlow::TypeTracker::end()) }
134189
}
135190
}
136191

192+
/**
193+
* Provides models for the `flask.Response` class
194+
*
195+
* See https://flask.palletsprojects.com/en/1.1.x/api/#flask.Response.
196+
*/
197+
module Response {
198+
/** Gets a reference to the `flask.Response` class. */
199+
private DataFlow::Node classRef(DataFlow::TypeTracker t) {
200+
t.start() and
201+
result in [flask_attr("Response"), flask::Flask::response_class()]
202+
or
203+
exists(DataFlow::TypeTracker t2 | result = classRef(t2).track(t2, t))
204+
}
205+
206+
/** Gets a reference to the `flask.Response` class. */
207+
DataFlow::Node classRef() { result = classRef(DataFlow::TypeTracker::end()) }
208+
209+
/**
210+
* A source of an instance of `flask.Response`.
211+
*
212+
* This can include instantiation of the class, return value from function
213+
* calls, or a special parameter that will be set when functions are call by external
214+
* library.
215+
*
216+
* Use `Response::instance()` predicate to get references to instances of `flask.Response`.
217+
*/
218+
abstract class InstanceSource extends HTTP::Server::HttpResponse::Range, DataFlow::Node { }
219+
220+
/** A direct instantiation of `flask.Response`. */
221+
private class ClassInstantiation extends InstanceSource, DataFlow::CfgNode {
222+
override CallNode node;
223+
224+
ClassInstantiation() { node.getFunction() = classRef().asCfgNode() }
225+
226+
override DataFlow::Node getBody() { result.asCfgNode() = node.getArg(0) }
227+
228+
override string getMimetypeDefault() { result = "text/html" }
229+
230+
/** Gets the argument passed to the `mimetype` parameter, if any. */
231+
private DataFlow::Node getMimetypeArg() {
232+
result.asCfgNode() in [node.getArg(3), node.getArgByName("mimetype")]
233+
}
234+
235+
/** Gets the argument passed to the `content_type` parameter, if any. */
236+
private DataFlow::Node getContentTypeArg() {
237+
result.asCfgNode() in [node.getArg(4), node.getArgByName("content_type")]
238+
}
239+
240+
override DataFlow::Node getMimetypeOrContentTypeArg() {
241+
result = this.getContentTypeArg()
242+
or
243+
// content_type argument takes priority over mimetype argument
244+
not exists(this.getContentTypeArg()) and
245+
result = this.getMimetypeArg()
246+
}
247+
}
248+
249+
/** Gets a reference to an instance of `flask.Response`. */
250+
private DataFlow::Node instance(DataFlow::TypeTracker t) {
251+
t.start() and
252+
result instanceof InstanceSource
253+
or
254+
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
255+
}
256+
257+
/** Gets a reference to an instance of `flask.Response`. */
258+
DataFlow::Node instance() { result = instance(DataFlow::TypeTracker::end()) }
259+
}
260+
137261
// ---------------------------------------------------------------------------
138262
// routing modeling
139263
// ---------------------------------------------------------------------------
@@ -324,8 +448,50 @@ private module FlaskModel {
324448
private class RequestInputFiles extends RequestInputMultiDict {
325449
RequestInputFiles() { attr_name = "files" }
326450
}
451+
327452
// TODO: Somehow specify that elements of `RequestInputFiles` are
328453
// Werkzeug::werkzeug::datastructures::FileStorage and should have those additional taint steps
329454
// AND that the 0-indexed argument to its' save method is a sink for path-injection.
330455
// https://werkzeug.palletsprojects.com/en/1.0.x/datastructures/#werkzeug.datastructures.FileStorage.save
456+
// ---------------------------------------------------------------------------
457+
// Response modeling
458+
// ---------------------------------------------------------------------------
459+
/**
460+
* A call to either `flask.make_response` function, or the `make_response` method on
461+
* an instance of `flask.Flask`.
462+
*
463+
* See
464+
* - https://flask.palletsprojects.com/en/1.1.x/api/#flask.Flask.make_response
465+
* - https://flask.palletsprojects.com/en/1.1.x/api/#flask.make_response
466+
*/
467+
private class FlaskMakeResponseCall extends HTTP::Server::HttpResponse::Range, DataFlow::CfgNode {
468+
override CallNode node;
469+
470+
FlaskMakeResponseCall() {
471+
node.getFunction() = flask::make_response().asCfgNode()
472+
or
473+
node.getFunction() = flask::Flask::make_response_().asCfgNode()
474+
}
475+
476+
override DataFlow::Node getBody() { result.asCfgNode() = node.getArg(0) }
477+
478+
override string getMimetypeDefault() { result = "text/html" }
479+
480+
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
481+
}
482+
483+
private class FlaskRouteHandlerReturn extends HTTP::Server::HttpResponse::Range, DataFlow::CfgNode {
484+
FlaskRouteHandlerReturn() {
485+
exists(Function routeHandler |
486+
routeHandler = any(FlaskRouteSetup rs).getARouteHandler() and
487+
node = routeHandler.getAReturnValueFlowNode()
488+
)
489+
}
490+
491+
override DataFlow::Node getBody() { result = this }
492+
493+
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
494+
495+
override string getMimetypeDefault() { result = "text/html" }
496+
}
331497
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,12 @@
11
import python
22
import experimental.meta.ConceptsTest
3+
4+
class DedicatedFlaskResponseTest extends HttpServerHttpResponseTest {
5+
DedicatedFlaskResponseTest() { file.getShortName() = "response_test.py" }
6+
}
7+
8+
class OtherFlaskResponseTest extends HttpServerHttpResponseTest {
9+
OtherFlaskResponseTest() { not this instanceof DedicatedFlaskResponseTest }
10+
11+
override string getARelevantTag() { result = "HttpResponse" }
12+
}

0 commit comments

Comments
 (0)