Skip to content

Commit 4cb2f2e

Browse files
committed
Python: Proper models of flask MethodView classes
1 parent e327fdb commit 4cb2f2e

File tree

3 files changed

+40
-6
lines changed

3 files changed

+40
-6
lines changed

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,27 @@ private module FlaskModel {
256256
/** Gets a reference to the `flask.views.View` class or any subclass. */
257257
DataFlow::Node subclassRef() { result = subclassRef(DataFlow::TypeTracker::end()) }
258258
}
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+
}
259280
}
260281
}
261282

@@ -377,6 +398,19 @@ private module FlaskModel {
377398
DataFlow::Node asViewResult() { result = asViewResult(DataFlow::TypeTracker::end()) }
378399
}
379400

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+
380414
private string werkzeug_rule_re() {
381415
// since flask uses werkzeug internally, we are using its routing rules from
382416
// https://github.com/pallets/werkzeug/blob/4dc8d6ab840d4b78cbd5789cef91b01e3bde01d5/src/werkzeug/routing.py#L138-L151

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: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,23 +59,23 @@ def dispatch_request(self, foo, not_routed=42): # $ requestHandler routedParame
5959

6060
class UserAPI(MethodView):
6161

62-
def get(self, user_id): # $ MISSING: requestHandler routedParameter=user_id
62+
def get(self, user_id): # $ requestHandler routedParameter=user_id
6363
if user_id is None:
6464
# return a list of users
6565
pass
6666
else:
6767
# expose a single user
6868
pass
6969

70-
def post(self): # $ MISSING: requestHandler
70+
def post(self): # $ requestHandler
7171
# create a new user
7272
pass
7373

74-
def delete(self, user_id): # $ MISSING: requestHandler routedParameter=user_id
74+
def delete(self, user_id): # $ requestHandler routedParameter=user_id
7575
# delete a single user
7676
pass
7777

78-
def put(self, user_id): # $ MISSING: requestHandler routedParameter=user_id
78+
def put(self, user_id): # $ requestHandler routedParameter=user_id
7979
# update a single user
8080
pass
8181

@@ -89,7 +89,7 @@ def put(self, user_id): # $ MISSING: requestHandler routedParameter=user_id
8989
class WithoutKnownRoute2(MethodView):
9090
# For handler without known route, treat all parameters as routed parameters
9191
# (accepting that there might be a few FPs)
92-
def get(self, foo, not_routed=42): # $ MISSING: requestHandler routedParameter=foo
92+
def get(self, foo, not_routed=42): # $ requestHandler routedParameter=foo SPURIOUS: routedParameter=not_routed
9393
pass
9494

9595

0 commit comments

Comments
 (0)