Skip to content

Commit 8cd9fde

Browse files
committed
Python: Model flask_admin
1 parent ab88d94 commit 8cd9fde

File tree

6 files changed

+103
-20
lines changed

6 files changed

+103
-20
lines changed

docs/codeql/support/reusables/frameworks.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ Python built-in support
159159
Flask, Web framework
160160
Tornado, Web framework
161161
Twisted, Web framework
162+
Flask-Admin, Web framework
162163
starlette, Asynchronous Server Gateway Interface (ASGI)
163164
dill, Serialization
164165
PyYAML, Serialization
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lgtm,codescanning
2+
* Added modeling of HTTP requests and responses when using `flask_admin` (`Flask-Admin` PyPI package), which leads to additional remote flow sources.

python/ql/lib/semmle/python/Frameworks.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ private import semmle.python.frameworks.Django
1515
private import semmle.python.frameworks.Fabric
1616
private import semmle.python.frameworks.FastApi
1717
private import semmle.python.frameworks.Flask
18+
private import semmle.python.frameworks.FlaskAdmin
1819
private import semmle.python.frameworks.FlaskSqlAlchemy
1920
private import semmle.python.frameworks.Idna
2021
private import semmle.python.frameworks.Invoke

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ module Flask {
238238
}
239239

240240
/** A route setup made by flask (sharing handling of URL patterns). */
241-
abstract private class FlaskRouteSetup extends HTTP::Server::RouteSetup::Range {
241+
abstract class FlaskRouteSetup extends HTTP::Server::RouteSetup::Range {
242242
override Parameter getARoutedParameter() {
243243
// If we don't know the URL pattern, we simply mark all parameters as a routed
244244
// parameter. This should give us more RemoteFlowSources but could also lead to
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* Provides classes modeling security-relevant aspects of the `Flask-Admin` PyPI package
3+
* (imported as `flask_admin`).
4+
*
5+
* See
6+
* - https://flask-admin.readthedocs.io/en/latest/
7+
* - https://pypi.org/project/Flask-Admin/
8+
*/
9+
10+
private import python
11+
private import semmle.python.dataflow.new.DataFlow
12+
private import semmle.python.dataflow.new.RemoteFlowSources
13+
private import semmle.python.dataflow.new.TaintTracking
14+
private import semmle.python.Concepts
15+
private import semmle.python.frameworks.Flask
16+
private import semmle.python.ApiGraphs
17+
18+
/**
19+
* Provides models for the `Flask-Admin` PyPI package (imported as `flask_admin`).
20+
*
21+
* See
22+
* - https://flask-admin.readthedocs.io/en/latest/
23+
* - https://pypi.org/project/Flask-Admin/
24+
*/
25+
private module FlaskAdmin {
26+
/**
27+
* A call to `flask_admin.expose`, which should be used as a decorator to make the
28+
* function exposed in the admin interface (and make it a request handler)
29+
*
30+
* See https://flask-admin.readthedocs.io/en/latest/api/mod_base/#flask_admin.base.expose
31+
*/
32+
private class FlaskAdminExposeCall extends Flask::FlaskRouteSetup, DataFlow::CallCfgNode {
33+
FlaskAdminExposeCall() {
34+
this = API::moduleImport("flask_admin").getMember("expose").getACall()
35+
}
36+
37+
override DataFlow::Node getUrlPatternArg() {
38+
result in [this.getArg(0), this.getArgByName("url")]
39+
}
40+
41+
override Function getARequestHandler() { result.getADecorator().getAFlowNode() = node }
42+
}
43+
44+
/**
45+
* A call to `flask_admin.expose_plugview`, which should be used as a decorator to make the
46+
* class (which should a flask View class) exposed in the admin interface.
47+
*
48+
* See https://flask-admin.readthedocs.io/en/latest/api/mod_base/#flask_admin.base.expose_plugview
49+
*/
50+
private class FlaskAdminExposePlugviewCall extends Flask::FlaskRouteSetup, DataFlow::CallCfgNode {
51+
FlaskAdminExposePlugviewCall() {
52+
this = API::moduleImport("flask_admin").getMember("expose_plugview").getACall()
53+
}
54+
55+
override DataFlow::Node getUrlPatternArg() {
56+
result in [this.getArg(0), this.getArgByName("url")]
57+
}
58+
59+
override Parameter getARoutedParameter() {
60+
result = super.getARoutedParameter() and
61+
(
62+
exists(this.getUrlPattern())
63+
or
64+
// the first argument is `self`, and the second argument `cls` will receive the
65+
// containing flask_admin View class -- this is only relevant if the URL pattern
66+
// is not known
67+
not exists(this.getUrlPattern()) and
68+
not result = this.getARequestHandler().getArg([0, 1])
69+
)
70+
}
71+
72+
override Function getARequestHandler() {
73+
exists(Flask::FlaskViewClass cls |
74+
cls.getADecorator().getAFlowNode() = node and
75+
result = cls.getARequestHandler()
76+
)
77+
}
78+
}
79+
}

python/ql/test/library-tests/frameworks/flask_admin/test.py

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,36 +13,36 @@
1313

1414

1515
class ExampleClass(flask_admin.BaseView):
16-
@flask_admin.expose('/')
17-
def foo(self): # $ MISSING: requestHandler
18-
return "foo"
16+
@flask_admin.expose('/') # $ routeSetup="/"
17+
def foo(self): # $ requestHandler
18+
return "foo" # $ HttpResponse
1919

20-
@flask_admin.expose(url='/bar/<arg>')
21-
def bar(self, arg): # $ MISSING: requestHandler
22-
ensure_tainted(arg) # $ MISSING: tainted
23-
return "bar: " + arg
20+
@flask_admin.expose(url='/bar/<arg>') # $ routeSetup="/bar/<arg>"
21+
def bar(self, arg): # $ requestHandler routedParameter=arg
22+
ensure_tainted(arg) # $ tainted
23+
return "bar: " + arg # $ HttpResponse
2424

25-
@flask_admin.expose_plugview("/flask-class")
26-
@flask_admin.expose_plugview(url="/flask-class/<arg>")
25+
@flask_admin.expose_plugview("/flask-class") # $ routeSetup="/flask-class"
26+
@flask_admin.expose_plugview(url="/flask-class/<arg>") # $ routeSetup="/flask-class/<arg>"
2727
class Nested(MethodView):
28-
def get(self, cls, arg="default"): # $ requestHandler routedParameter=arg SPURIOUS: routedParameter=cls
28+
def get(self, cls, arg="default"): # $ requestHandler routedParameter=arg
2929
assert isinstance(cls, ExampleClass)
3030
ensure_tainted(arg) # $ tainted
31-
ensure_not_tainted(cls) # $ SPURIOUS: tainted
32-
return "GET: " + arg
31+
ensure_not_tainted(cls)
32+
return "GET: " + arg # $ HttpResponse
3333

34-
def post(self, cls, arg): # $ requestHandler routedParameter=arg SPURIOUS: routedParameter=cls
34+
def post(self, cls, arg): # $ requestHandler routedParameter=arg
3535
assert isinstance(cls, ExampleClass)
3636
ensure_tainted(arg) # $ tainted
37-
ensure_not_tainted(cls) # $ SPURIOUS: tainted
38-
return "POST: " + arg
37+
ensure_not_tainted(cls)
38+
return "POST: " + arg # $ HttpResponse
3939

40-
@flask_admin.expose_plugview(UNKNOWN_ROUTE)
40+
@flask_admin.expose_plugview(UNKNOWN_ROUTE) # $ routeSetup
4141
class WithUnknownRoute(MethodView):
42-
def get(self, cls, maybeRouted): # $ requestHandler routedParameter=maybeRouted SPURIOUS: routedParameter=cls
42+
def get(self, cls, maybeRouted): # $ requestHandler routedParameter=maybeRouted
4343
ensure_tainted(maybeRouted) # $ tainted
44-
ensure_not_tainted(cls) # $ SPURIOUS: tainted
45-
return "ok"
44+
ensure_not_tainted(cls)
45+
return "ok" # $ HttpResponse
4646

4747

4848
@app.route('/') # $ routeSetup="/"

0 commit comments

Comments
 (0)