Skip to content

Commit b5d40e4

Browse files
authored
Merge pull request github#4944 from RasmusWL/flask-class-based-handlers
Python: Add modeling of Flask class based (HTTP) request handlers
2 parents de8ac6c + 4cb2f2e commit b5d40e4

File tree

3 files changed

+248
-8
lines changed

3 files changed

+248
-8
lines changed

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

Lines changed: 178 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ private module FlaskModel {
3434
* WARNING: Only holds for a few predefined attributes.
3535
*/
3636
private DataFlow::Node flask_attr(DataFlow::TypeTracker t, string attr_name) {
37-
attr_name in ["request", "make_response", "Response"] and
37+
attr_name in ["request", "make_response", "Response", "views"] and
3838
(
3939
t.start() and
4040
result = DataFlow::importNode("flask" + "." + attr_name)
@@ -187,6 +187,97 @@ private module FlaskModel {
187187
*/
188188
DataFlow::Node response_class() { result = response_class(DataFlow::TypeTracker::end()) }
189189
}
190+
191+
// -------------------------------------------------------------------------
192+
// flask.views
193+
// -------------------------------------------------------------------------
194+
/** Gets a reference to the `flask.views` module. */
195+
DataFlow::Node views() { result = flask_attr("views") }
196+
197+
/** Provides models for the `flask.views` module */
198+
module views {
199+
/**
200+
* Gets a reference to the attribute `attr_name` of the `flask.views` module.
201+
* WARNING: Only holds for a few predefined attributes.
202+
*/
203+
private DataFlow::Node views_attr(DataFlow::TypeTracker t, string attr_name) {
204+
attr_name in ["View", "MethodView"] and
205+
(
206+
t.start() and
207+
result = DataFlow::importNode("flask.views" + "." + attr_name)
208+
or
209+
t.startInAttr(attr_name) and
210+
result = views()
211+
)
212+
or
213+
// Due to bad performance when using normal setup with `views_attr(t2, attr_name).track(t2, t)`
214+
// we have inlined that code and forced a join
215+
exists(DataFlow::TypeTracker t2 |
216+
exists(DataFlow::StepSummary summary |
217+
views_attr_first_join(t2, attr_name, result, summary) and
218+
t = t2.append(summary)
219+
)
220+
)
221+
}
222+
223+
pragma[nomagic]
224+
private predicate views_attr_first_join(
225+
DataFlow::TypeTracker t2, string attr_name, DataFlow::Node res,
226+
DataFlow::StepSummary summary
227+
) {
228+
DataFlow::StepSummary::step(views_attr(t2, attr_name), res, summary)
229+
}
230+
231+
/**
232+
* Gets a reference to the attribute `attr_name` of the `flask.views` module.
233+
* WARNING: Only holds for a few predefined attributes.
234+
*/
235+
private DataFlow::Node views_attr(string attr_name) {
236+
result = views_attr(DataFlow::TypeTracker::end(), attr_name)
237+
}
238+
239+
/**
240+
* Provides models for the `flask.views.View` class and subclasses.
241+
*
242+
* See https://flask.palletsprojects.com/en/1.1.x/views/#basic-principle.
243+
*/
244+
module View {
245+
/** Gets a reference to the `flask.views.View` class or any subclass. */
246+
private DataFlow::Node subclassRef(DataFlow::TypeTracker t) {
247+
t.start() and
248+
result = views_attr(["View", "MethodView"])
249+
or
250+
// subclasses in project code
251+
result.asExpr().(ClassExpr).getABase() = subclassRef(t.continue()).asExpr()
252+
or
253+
exists(DataFlow::TypeTracker t2 | result = subclassRef(t2).track(t2, t))
254+
}
255+
256+
/** Gets a reference to the `flask.views.View` class or any subclass. */
257+
DataFlow::Node subclassRef() { result = subclassRef(DataFlow::TypeTracker::end()) }
258+
}
259+
260+
/**
261+
* Provides models for the `flask.views.MethodView` class and subclasses.
262+
*
263+
* See https://flask.palletsprojects.com/en/1.1.x/views/#method-based-dispatching.
264+
*/
265+
module MethodView {
266+
/** Gets a reference to the `flask.views.View` class or any subclass. */
267+
private DataFlow::Node subclassRef(DataFlow::TypeTracker t) {
268+
t.start() and
269+
result = views_attr("MethodView")
270+
or
271+
// subclasses in project code
272+
result.asExpr().(ClassExpr).getABase() = subclassRef(t.continue()).asExpr()
273+
or
274+
exists(DataFlow::TypeTracker t2 | result = subclassRef(t2).track(t2, t))
275+
}
276+
277+
/** Gets a reference to the `flask.views.View` class or any subclass. */
278+
DataFlow::Node subclassRef() { result = subclassRef(DataFlow::TypeTracker::end()) }
279+
}
280+
}
190281
}
191282

192283
/**
@@ -261,6 +352,65 @@ private module FlaskModel {
261352
// ---------------------------------------------------------------------------
262353
// routing modeling
263354
// ---------------------------------------------------------------------------
355+
/** A flask View class defined in project code. */
356+
class FlaskViewClassDef extends Class {
357+
FlaskViewClassDef() { this.getABase() = flask::views::View::subclassRef().asExpr() }
358+
359+
/** Gets a function that could handle incoming requests, if any. */
360+
Function getARequestHandler() {
361+
// TODO: This doesn't handle attribute assignment. Should be OK, but analysis is not as complete as with
362+
// points-to and `.lookup`, which would handle `post = my_post_handler` inside class def
363+
result = this.getAMethod() and
364+
result.getName() = "dispatch_request"
365+
}
366+
367+
/** Gets a reference to this class. */
368+
private DataFlow::Node getARef(DataFlow::TypeTracker t) {
369+
t.start() and
370+
result.asExpr().(ClassExpr) = this.getParent()
371+
or
372+
exists(DataFlow::TypeTracker t2 | result = this.getARef(t2).track(t2, t))
373+
}
374+
375+
/** Gets a reference to this class. */
376+
DataFlow::Node getARef() { result = this.getARef(DataFlow::TypeTracker::end()) }
377+
378+
/** Gets a reference to the `as_view` classmethod of this class. */
379+
private DataFlow::Node asViewRef(DataFlow::TypeTracker t) {
380+
t.startInAttr("as_view") and
381+
result = this.getARef()
382+
or
383+
exists(DataFlow::TypeTracker t2 | result = this.asViewRef(t2).track(t2, t))
384+
}
385+
386+
/** Gets a reference to the `as_view` classmethod of this class. */
387+
DataFlow::Node asViewRef() { result = this.asViewRef(DataFlow::TypeTracker::end()) }
388+
389+
/** Gets a reference to the result of calling the `as_view` classmethod of this class. */
390+
private DataFlow::Node asViewResult(DataFlow::TypeTracker t) {
391+
t.start() and
392+
result.asCfgNode().(CallNode).getFunction() = this.asViewRef().asCfgNode()
393+
or
394+
exists(DataFlow::TypeTracker t2 | result = asViewResult(t2).track(t2, t))
395+
}
396+
397+
/** Gets a reference to the result of calling the `as_view` classmethod of this class. */
398+
DataFlow::Node asViewResult() { result = asViewResult(DataFlow::TypeTracker::end()) }
399+
}
400+
401+
class FlaskMethodViewClassDef extends FlaskViewClassDef {
402+
FlaskMethodViewClassDef() { this.getABase() = flask::views::MethodView::subclassRef().asExpr() }
403+
404+
override Function getARequestHandler() {
405+
result = super.getARequestHandler()
406+
or
407+
// TODO: This doesn't handle attribute assignment. Should be OK, but analysis is not as complete as with
408+
// points-to and `.lookup`, which would handle `post = my_post_handler` inside class def
409+
result = this.getAMethod() and
410+
result.getName() = HTTP::httpVerbLower()
411+
}
412+
}
413+
264414
private string werkzeug_rule_re() {
265415
// since flask uses werkzeug internally, we are using its routing rules from
266416
// https://github.com/pallets/werkzeug/blob/4dc8d6ab840d4b78cbd5789cef91b01e3bde01d5/src/werkzeug/routing.py#L138-L151
@@ -318,12 +468,36 @@ private module FlaskModel {
318468
result.asCfgNode() in [node.getArg(0), node.getArgByName("rule")]
319469
}
320470

471+
DataFlow::Node getViewArg() {
472+
result.asCfgNode() in [node.getArg(2), node.getArgByName("view_func")]
473+
}
474+
321475
override Function getARequestHandler() {
322-
exists(DataFlow::Node view_func_arg, DataFlow::LocalSourceNode func_src |
323-
view_func_arg.asCfgNode() in [node.getArg(2), node.getArgByName("view_func")] and
324-
func_src.flowsTo(view_func_arg) and
476+
exists(DataFlow::LocalSourceNode func_src |
477+
func_src.flowsTo(getViewArg()) and
325478
func_src.asExpr().(CallableExpr) = result.getDefinition()
326479
)
480+
or
481+
exists(FlaskViewClassDef vc |
482+
getViewArg() = vc.asViewResult() and
483+
result = vc.getARequestHandler()
484+
)
485+
}
486+
}
487+
488+
/** A request handler defined in a django view class, that has no known route. */
489+
private class FlaskViewClassHandlerWithoutKnownRoute extends HTTP::Server::RequestHandler::Range {
490+
FlaskViewClassHandlerWithoutKnownRoute() {
491+
exists(FlaskViewClassDef vc | vc.getARequestHandler() = this) and
492+
not exists(FlaskRouteSetup setup | setup.getARequestHandler() = this)
493+
}
494+
495+
override Parameter getARoutedParameter() {
496+
// Since we don't know the URL pattern, we simply mark all parameters as a routed
497+
// parameter. This should give us more RemoteFlowSources but could also lead to
498+
// more FPs. If this turns out to be the wrong tradeoff, we can always change our mind.
499+
result in [this.getArg(_), this.getArgByName(_)] and
500+
not result = this.getArg(0)
327501
}
328502
}
329503

python/ql/test/experimental/library-tests/frameworks/flask/old_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def hello_world(): # $requestHandler
1111

1212
class MyView(MethodView):
1313

14-
def get(self, user_id): # $ MISSING: requestHandler
14+
def get(self, user_id): # $ requestHandler
1515
if user_id is None:
1616
# return a list of users
1717
pass

python/ql/test/experimental/library-tests/frameworks/flask/routing_test.py

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,76 @@ def later_set(): # $ MISSING: requestHandler
2121
app.add_url_rule('/later-set', 'later_set', view_func=None) # $routeSetup="/later-set"
2222
app.view_functions['later_set'] = later_set
2323

24+
# We don't want to execute this at runtime (since program will crash). Just using
25+
# `False` makes our analysis skip it, so here's a workaround :D
26+
if __file__ == "False":
27+
@app.route(UNKNOWN_ROUTE) # $routeSetup
28+
def unkown_route(foo, bar): # $requestHandler routedParameter=foo routedParameter=bar
29+
return make_response("unkown_route") # $HttpResponse
2430

25-
@app.route(UNKNOWN_ROUTE) # $routeSetup
26-
def unkown_route(foo, bar): # $requestHandler routedParameter=foo routedParameter=bar
27-
return make_response("unkown_route") # $HttpResponse
31+
# View
32+
#
33+
# see https://flask.palletsprojects.com/en/1.1.x/views/#basic-principle
34+
35+
from flask.views import View
36+
37+
38+
class ShowUser(View):
39+
40+
def dispatch_request(self, user_id): # $ requestHandler routedParameter=user_id
41+
return "user_id: {}".format(user_id) # $ HttpResponse
42+
43+
app.add_url_rule("/basic/user/<int:user_id>", view_func=ShowUser.as_view('show_user')) # $routeSetup="/basic/user/<int:user_id>"
44+
45+
46+
class WithoutKnownRoute1(View):
47+
# For handler without known route, treat all parameters as routed parameters
48+
# (accepting that there might be a few FPs)
49+
def dispatch_request(self, foo, not_routed=42): # $ requestHandler routedParameter=foo SPURIOUS: routedParameter=not_routed
50+
pass
51+
52+
53+
# MethodView
54+
#
55+
# see https://flask.palletsprojects.com/en/1.1.x/views/#method-views-for-apis
56+
57+
from flask.views import MethodView
58+
59+
60+
class UserAPI(MethodView):
61+
62+
def get(self, user_id): # $ requestHandler routedParameter=user_id
63+
if user_id is None:
64+
# return a list of users
65+
pass
66+
else:
67+
# expose a single user
68+
pass
69+
70+
def post(self): # $ requestHandler
71+
# create a new user
72+
pass
73+
74+
def delete(self, user_id): # $ requestHandler routedParameter=user_id
75+
# delete a single user
76+
pass
77+
78+
def put(self, user_id): # $ requestHandler routedParameter=user_id
79+
# update a single user
80+
pass
81+
82+
83+
user_view = UserAPI.as_view("user_api")
84+
app.add_url_rule("/users/", defaults={"user_id": None}, view_func=user_view, methods=["GET",]) # $routeSetup="/users/"
85+
app.add_url_rule("/users/", view_func=user_view, methods=["POST",]) # $routeSetup="/users/"
86+
app.add_url_rule("/users/<int:user_id>", view_func=user_view, methods=["GET", "PUT", "DELETE"]) # $routeSetup="/users/<int:user_id>"
87+
88+
89+
class WithoutKnownRoute2(MethodView):
90+
# For handler without known route, treat all parameters as routed parameters
91+
# (accepting that there might be a few FPs)
92+
def get(self, foo, not_routed=42): # $ requestHandler routedParameter=foo SPURIOUS: routedParameter=not_routed
93+
pass
2894

2995

3096
if __name__ == "__main__":

0 commit comments

Comments
 (0)