Skip to content

Commit cb94e7d

Browse files
authored
Merge pull request #2140 from RasmusWL/python-fix-flask
Python: Modernise flask + correctly handle flask.make_response
2 parents f813e06 + 5424666 commit cb94e7d

File tree

23 files changed

+242
-159
lines changed

23 files changed

+242
-159
lines changed

python/ql/src/Security/CWE-215/FlaskDebug.ql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import semmle.python.web.flask.General
1717

1818
from CallNode call, Object isTrue
1919
where
20-
call = theFlaskClass().declaredAttribute("run").(FunctionObject).getACall() and
20+
call = theFlaskClass().declaredAttribute("run").(FunctionValue).getACall() and
2121
call.getArgByName("debug").refersTo(isTrue) and
2222
isTrue.booleanValue() = true
2323
select call, "A Flask app appears to be run in debug mode. This may allow an attacker to run arbitrary code through the debugger."
Lines changed: 28 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,22 @@
11
import python
22
import semmle.python.web.Http
3-
4-
/** The flask module */
5-
ModuleObject theFlaskModule() {
6-
result = ModuleObject::named("flask")
7-
}
3+
import semmle.python.web.flask.Response
84

95
/** The flask app class */
10-
ClassObject theFlaskClass() {
11-
result = theFlaskModule().attr("Flask")
12-
}
6+
ClassValue theFlaskClass() { result = Value::named("flask.Flask") }
137

148
/** The flask MethodView class */
15-
ClassObject theFlaskMethodViewClass() {
16-
result = ModuleObject::named("flask.views").attr("MethodView")
17-
}
9+
ClassValue theFlaskMethodViewClass() { result = Value::named("flask.views.MethodView") }
1810

19-
ClassObject theFlaskReponseClass() {
20-
result = theFlaskModule().attr("Response")
21-
}
11+
ClassValue theFlaskReponseClass() { result = Value::named("flask.Response") }
2212

23-
/** Holds if `route` is routed to `func`
13+
/**
14+
* Holds if `route` is routed to `func`
2415
* by decorating `func` with `app.route(route)`
2516
*/
2617
predicate app_route(ControlFlowNode route, Function func) {
2718
exists(CallNode route_call, CallNode decorator_call |
28-
route_call.getFunction().(AttrNode).getObject("route").refersTo(_, theFlaskClass(), _) and
19+
route_call.getFunction().(AttrNode).getObject("route").pointsTo().getClass() = theFlaskClass() and
2920
decorator_call.getFunction() = route_call and
3021
route_call.getArg(0) = route and
3122
decorator_call.getArg(0).getNode().(FunctionExpr).getInnerScope() = func
@@ -35,30 +26,29 @@ predicate app_route(ControlFlowNode route, Function func) {
3526
/* Helper for add_url_rule */
3627
private predicate add_url_rule_call(ControlFlowNode regex, ControlFlowNode callable) {
3728
exists(CallNode call |
38-
call.getFunction().(AttrNode).getObject("add_url_rule").refersTo(_, theFlaskClass(), _) and
39-
regex = call.getArg(0) |
29+
call.getFunction().(AttrNode).getObject("add_url_rule").pointsTo().getClass() = theFlaskClass() and
30+
regex = call.getArg(0)
31+
|
4032
callable = call.getArg(2) or
4133
callable = call.getArgByName("view_func")
4234
)
4335
}
4436

4537
/** Holds if urls matching `regex` are routed to `func` */
4638
predicate add_url_rule(ControlFlowNode regex, Function func) {
47-
exists(ControlFlowNode callable |
48-
add_url_rule_call(regex, callable)
49-
|
50-
exists(PyFunctionObject f | f.getFunction() = func and callable.refersTo(f))
39+
exists(ControlFlowNode callable | add_url_rule_call(regex, callable) |
40+
exists(PythonFunctionValue f | f.getScope() = func and callable.pointsTo(f))
5141
or
5242
/* MethodView.as_view() */
53-
exists(MethodViewClass view_cls |
54-
view_cls.asTaint().taints(callable) |
55-
func = view_cls.lookupAttribute(httpVerbLower()).(FunctionObject).getFunction()
43+
exists(MethodViewClass view_cls | view_cls.asTaint().taints(callable) |
44+
func = view_cls.lookup(httpVerbLower()).(FunctionValue).getScope()
5645
)
57-
/* TO DO -- Handle Views that aren't MethodViews */
46+
/* TODO: -- Handle Views that aren't MethodViews */
5847
)
5948
}
6049

61-
/** Holds if urls matching `regex` are routed to `func` using
50+
/**
51+
* Holds if urls matching `regex` are routed to `func` using
6252
* any of flask's routing mechanisms.
6353
*/
6454
predicate flask_routing(ControlFlowNode regex, Function func) {
@@ -68,65 +58,47 @@ predicate flask_routing(ControlFlowNode regex, Function func) {
6858
}
6959

7060
/** A class that extends flask.views.MethodView */
71-
private class MethodViewClass extends ClassObject {
72-
73-
MethodViewClass() {
74-
this.getAnImproperSuperType() = theFlaskMethodViewClass()
75-
}
61+
private class MethodViewClass extends ClassValue {
62+
MethodViewClass() { this.getASuperType() = theFlaskMethodViewClass() }
7663

7764
/* As we are restricted to strings for taint kinds, we need to map these classes to strings. */
78-
string taintString() {
79-
result = "flask/" + this.getQualifiedName() + ".as.view"
80-
}
65+
string taintString() { result = "flask/" + this.getQualifiedName() + ".as.view" }
8166

8267
/* As we are restricted to strings for taint kinds, we need to map these classes to strings. */
83-
TaintKind asTaint() {
84-
result = this.taintString()
85-
}
68+
TaintKind asTaint() { result = this.taintString() }
8669
}
8770

8871
private class MethodViewTaint extends TaintKind {
89-
90-
MethodViewTaint() {
91-
any(MethodViewClass cls).taintString() = this
92-
}
72+
MethodViewTaint() { any(MethodViewClass cls).taintString() = this }
9373
}
9474

9575
/** A source of method view "taint"s. */
9676
private class AsView extends TaintSource {
97-
9877
AsView() {
99-
exists(ClassObject view_class |
100-
view_class.getAnImproperSuperType() = theFlaskMethodViewClass() and
101-
this.(CallNode).getFunction().(AttrNode).getObject("as_view").refersTo(view_class)
78+
exists(ClassValue view_class |
79+
view_class.getASuperType() = theFlaskMethodViewClass() and
80+
this.(CallNode).getFunction().(AttrNode).getObject("as_view").pointsTo(view_class)
10281
)
10382
}
10483

105-
override string toString() {
106-
result = "flask.MethodView.as_view()"
107-
}
84+
override string toString() { result = "flask.MethodView.as_view()" }
10885

10986
override predicate isSourceOf(TaintKind kind) {
11087
exists(MethodViewClass view_class |
11188
kind = view_class.asTaint() and
112-
this.(CallNode).getFunction().(AttrNode).getObject("as_view").refersTo(view_class)
89+
this.(CallNode).getFunction().(AttrNode).getObject("as_view").pointsTo(view_class)
11390
)
11491
}
115-
11692
}
11793

118-
11994
class FlaskCookieSet extends CookieSet, CallNode {
120-
12195
FlaskCookieSet() {
122-
this.getFunction().(AttrNode).getObject("set_cookie").refersTo(_, theFlaskReponseClass(), _)
96+
any(FlaskResponseTaintKind t).taints(this.getFunction().(AttrNode).getObject("set_cookie"))
12397
}
12498

12599
override string toString() { result = CallNode.super.toString() }
126100

127101
override ControlFlowNode getKey() { result = this.getArg(0) }
128102

129103
override ControlFlowNode getValue() { result = this.getArg(1) }
130-
131-
132104
}
Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,26 @@
1-
/** Provides class representing the `flask.redirect` function.
1+
/**
2+
* Provides class representing the `flask.redirect` function.
23
* This module is intended to be imported into a taint-tracking query
34
* to extend `TaintSink`.
45
*/
5-
import python
66

7+
import python
78
import semmle.python.security.TaintTracking
89
import semmle.python.security.strings.Basic
910
import semmle.python.web.flask.General
1011

11-
FunctionObject flask_redirect() {
12-
result = theFlaskModule().attr("redirect")
13-
}
12+
FunctionValue flask_redirect() { result = Value::named("flask.redirect") }
1413

1514
/**
1615
* Represents an argument to the `flask.redirect` function.
1716
*/
1817
class FlaskRedirect extends HttpRedirectTaintSink {
19-
20-
override string toString() {
21-
result = "flask.redirect"
22-
}
18+
override string toString() { result = "flask.redirect" }
2319

2420
FlaskRedirect() {
2521
exists(CallNode call |
2622
flask_redirect().getACall() = call and
2723
this = call.getAnArg()
2824
)
2925
}
30-
3126
}
Lines changed: 21 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,56 @@
11
import python
2-
32
import semmle.python.security.TaintTracking
43
import semmle.python.web.Http
54
import semmle.python.web.flask.General
65

7-
private Object theFlaskRequestObject() {
8-
result = theFlaskModule().attr("request")
9-
10-
}
6+
private Value theFlaskRequestObject() { result = Value::named("flask.request") }
117

128
/** Holds if `attr` is an access of attribute `name` of the flask request object */
139
private predicate flask_request_attr(AttrNode attr, string name) {
1410
attr.isLoad() and
15-
attr.getObject(name).refersTo(theFlaskRequestObject())
11+
attr.getObject(name).pointsTo(theFlaskRequestObject())
1612
}
1713

1814
/** Source of external data from a flask request */
1915
class FlaskRequestData extends HttpRequestTaintSource {
20-
2116
FlaskRequestData() {
2217
not this instanceof FlaskRequestArgs and
23-
exists(string name |
24-
flask_request_attr(this, name) |
25-
name = "path" or name = "full_path" or
26-
name = "base_url" or name = "url"
18+
exists(string name | flask_request_attr(this, name) |
19+
name = "path" or
20+
name = "full_path" or
21+
name = "base_url" or
22+
name = "url"
2723
)
2824
}
2925

30-
override predicate isSourceOf(TaintKind kind) {
31-
kind instanceof ExternalStringKind
32-
}
33-
34-
override string toString() {
35-
result = "flask.request"
36-
}
26+
override predicate isSourceOf(TaintKind kind) { kind instanceof ExternalStringKind }
3727

28+
override string toString() { result = "flask.request" }
3829
}
3930

4031
/** Source of dictionary whose values are externally controlled */
4132
class FlaskRequestArgs extends HttpRequestTaintSource {
42-
4333
FlaskRequestArgs() {
44-
exists(string attr |
45-
flask_request_attr(this, attr) |
46-
attr = "args" or attr = "form" or
47-
attr = "values" or attr = "files" or
48-
attr = "headers" or attr = "json"
34+
exists(string attr | flask_request_attr(this, attr) |
35+
attr = "args" or
36+
attr = "form" or
37+
attr = "values" or
38+
attr = "files" or
39+
attr = "headers" or
40+
attr = "json"
4941
)
5042
}
5143

52-
override predicate isSourceOf(TaintKind kind) {
53-
kind instanceof ExternalStringDictKind
54-
}
55-
56-
override string toString() {
57-
result = "flask.request.args"
58-
}
44+
override predicate isSourceOf(TaintKind kind) { kind instanceof ExternalStringDictKind }
5945

46+
override string toString() { result = "flask.request.args" }
6047
}
6148

62-
6349
/** Source of dictionary whose values are externally controlled */
6450
class FlaskRequestJson extends TaintSource {
51+
FlaskRequestJson() { flask_request_attr(this, "json") }
6552

66-
FlaskRequestJson() {
67-
flask_request_attr(this, "json")
68-
}
69-
70-
override predicate isSourceOf(TaintKind kind) {
71-
kind instanceof ExternalJsonKind
72-
}
73-
74-
override string toString() {
75-
result = "flask.request.json"
76-
}
53+
override predicate isSourceOf(TaintKind kind) { kind instanceof ExternalJsonKind }
7754

55+
override string toString() { result = "flask.request.json" }
7856
}
79-
Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,55 @@
11
import python
2-
3-
42
import semmle.python.security.TaintTracking
53
import semmle.python.security.strings.Basic
6-
74
import semmle.python.web.flask.General
85

9-
/** A flask response, which is vulnerable to any sort of
10-
* http response malice. */
6+
/**
7+
* A flask response, which is vulnerable to any sort of
8+
* http response malice.
9+
*/
1110
class FlaskRoutedResponse extends HttpResponseTaintSink {
12-
1311
FlaskRoutedResponse() {
1412
exists(PyFunctionObject response |
1513
flask_routing(_, response.getFunction()) and
1614
this = response.getAReturnedNode()
1715
)
1816
}
1917

20-
override predicate sinks(TaintKind kind) {
21-
kind instanceof StringKind
22-
}
23-
24-
override string toString() {
25-
result = "flask.routed.response"
26-
}
18+
override predicate sinks(TaintKind kind) { kind instanceof StringKind }
2719

20+
override string toString() { result = "flask.routed.response" }
2821
}
2922

30-
3123
class FlaskResponseArgument extends HttpResponseTaintSink {
32-
3324
FlaskResponseArgument() {
3425
exists(CallNode call |
35-
call.getFunction().refersTo(theFlaskReponseClass()) and
26+
(
27+
call.getFunction().pointsTo(theFlaskReponseClass())
28+
or
29+
call.getFunction().pointsTo(Value::named("flask.make_response"))
30+
) and
3631
call.getArg(0) = this
3732
)
3833
}
3934

40-
override predicate sinks(TaintKind kind) {
41-
kind instanceof StringKind
42-
}
35+
override predicate sinks(TaintKind kind) { kind instanceof StringKind }
4336

44-
override string toString() {
45-
result = "flask.response.argument"
46-
}
37+
override string toString() { result = "flask.response.argument" }
38+
}
39+
40+
class FlaskResponseTaintKind extends TaintKind {
41+
FlaskResponseTaintKind() { this = "flask.Response" }
42+
}
4743

48-
}
44+
class FlaskResponseConfiguration extends TaintTracking::Configuration {
45+
FlaskResponseConfiguration() { this = "Flask response configuration" }
46+
47+
override predicate isSource(DataFlow::Node node, TaintKind kind) {
48+
kind instanceof FlaskResponseTaintKind and
49+
(
50+
node.asCfgNode().(CallNode).getFunction().pointsTo(theFlaskReponseClass())
51+
or
52+
node.asCfgNode().(CallNode).getFunction().pointsTo(Value::named("flask.make_response"))
53+
)
54+
}
55+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
| / | Function hello |
2+
| /dangerous | Function dangerous |
3+
| /dangerous-with-cfg-split | Function dangerous2 |
4+
| /safe | Function safe |
5+
| /the/ | Function get |
6+
| /unsafe | Function unsafe |

0 commit comments

Comments
 (0)