Python: Model flask_admin

This commit is contained in:
Rasmus Wriedt Larsen
2021-11-02 15:40:58 +01:00
parent ab88d945e2
commit 8cd9fdebf9
6 changed files with 103 additions and 20 deletions

View File

@@ -0,0 +1,2 @@
lgtm,codescanning
* Added modeling of HTTP requests and responses when using `flask_admin` (`Flask-Admin` PyPI package), which leads to additional remote flow sources.

View File

@@ -15,6 +15,7 @@ private import semmle.python.frameworks.Django
private import semmle.python.frameworks.Fabric
private import semmle.python.frameworks.FastApi
private import semmle.python.frameworks.Flask
private import semmle.python.frameworks.FlaskAdmin
private import semmle.python.frameworks.FlaskSqlAlchemy
private import semmle.python.frameworks.Idna
private import semmle.python.frameworks.Invoke

View File

@@ -238,7 +238,7 @@ module Flask {
}
/** A route setup made by flask (sharing handling of URL patterns). */
abstract private class FlaskRouteSetup extends HTTP::Server::RouteSetup::Range {
abstract class FlaskRouteSetup extends HTTP::Server::RouteSetup::Range {
override Parameter getARoutedParameter() {
// If we don't know the URL pattern, we simply mark all parameters as a routed
// parameter. This should give us more RemoteFlowSources but could also lead to

View File

@@ -0,0 +1,79 @@
/**
* Provides classes modeling security-relevant aspects of the `Flask-Admin` PyPI package
* (imported as `flask_admin`).
*
* See
* - https://flask-admin.readthedocs.io/en/latest/
* - https://pypi.org/project/Flask-Admin/
*/
private import python
private import semmle.python.dataflow.new.DataFlow
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.dataflow.new.TaintTracking
private import semmle.python.Concepts
private import semmle.python.frameworks.Flask
private import semmle.python.ApiGraphs
/**
* Provides models for the `Flask-Admin` PyPI package (imported as `flask_admin`).
*
* See
* - https://flask-admin.readthedocs.io/en/latest/
* - https://pypi.org/project/Flask-Admin/
*/
private module FlaskAdmin {
/**
* A call to `flask_admin.expose`, which should be used as a decorator to make the
* function exposed in the admin interface (and make it a request handler)
*
* See https://flask-admin.readthedocs.io/en/latest/api/mod_base/#flask_admin.base.expose
*/
private class FlaskAdminExposeCall extends Flask::FlaskRouteSetup, DataFlow::CallCfgNode {
FlaskAdminExposeCall() {
this = API::moduleImport("flask_admin").getMember("expose").getACall()
}
override DataFlow::Node getUrlPatternArg() {
result in [this.getArg(0), this.getArgByName("url")]
}
override Function getARequestHandler() { result.getADecorator().getAFlowNode() = node }
}
/**
* A call to `flask_admin.expose_plugview`, which should be used as a decorator to make the
* class (which should a flask View class) exposed in the admin interface.
*
* See https://flask-admin.readthedocs.io/en/latest/api/mod_base/#flask_admin.base.expose_plugview
*/
private class FlaskAdminExposePlugviewCall extends Flask::FlaskRouteSetup, DataFlow::CallCfgNode {
FlaskAdminExposePlugviewCall() {
this = API::moduleImport("flask_admin").getMember("expose_plugview").getACall()
}
override DataFlow::Node getUrlPatternArg() {
result in [this.getArg(0), this.getArgByName("url")]
}
override Parameter getARoutedParameter() {
result = super.getARoutedParameter() and
(
exists(this.getUrlPattern())
or
// the first argument is `self`, and the second argument `cls` will receive the
// containing flask_admin View class -- this is only relevant if the URL pattern
// is not known
not exists(this.getUrlPattern()) and
not result = this.getARequestHandler().getArg([0, 1])
)
}
override Function getARequestHandler() {
exists(Flask::FlaskViewClass cls |
cls.getADecorator().getAFlowNode() = node and
result = cls.getARequestHandler()
)
}
}
}

View File

@@ -13,36 +13,36 @@ UNKNOWN_ROUTE = eval(foo) # $ getCode=foo
class ExampleClass(flask_admin.BaseView):
@flask_admin.expose('/')
def foo(self): # $ MISSING: requestHandler
return "foo"
@flask_admin.expose('/') # $ routeSetup="/"
def foo(self): # $ requestHandler
return "foo" # $ HttpResponse
@flask_admin.expose(url='/bar/<arg>')
def bar(self, arg): # $ MISSING: requestHandler
ensure_tainted(arg) # $ MISSING: tainted
return "bar: " + arg
@flask_admin.expose(url='/bar/<arg>') # $ routeSetup="/bar/<arg>"
def bar(self, arg): # $ requestHandler routedParameter=arg
ensure_tainted(arg) # $ tainted
return "bar: " + arg # $ HttpResponse
@flask_admin.expose_plugview("/flask-class")
@flask_admin.expose_plugview(url="/flask-class/<arg>")
@flask_admin.expose_plugview("/flask-class") # $ routeSetup="/flask-class"
@flask_admin.expose_plugview(url="/flask-class/<arg>") # $ routeSetup="/flask-class/<arg>"
class Nested(MethodView):
def get(self, cls, arg="default"): # $ requestHandler routedParameter=arg SPURIOUS: routedParameter=cls
def get(self, cls, arg="default"): # $ requestHandler routedParameter=arg
assert isinstance(cls, ExampleClass)
ensure_tainted(arg) # $ tainted
ensure_not_tainted(cls) # $ SPURIOUS: tainted
return "GET: " + arg
ensure_not_tainted(cls)
return "GET: " + arg # $ HttpResponse
def post(self, cls, arg): # $ requestHandler routedParameter=arg SPURIOUS: routedParameter=cls
def post(self, cls, arg): # $ requestHandler routedParameter=arg
assert isinstance(cls, ExampleClass)
ensure_tainted(arg) # $ tainted
ensure_not_tainted(cls) # $ SPURIOUS: tainted
return "POST: " + arg
ensure_not_tainted(cls)
return "POST: " + arg # $ HttpResponse
@flask_admin.expose_plugview(UNKNOWN_ROUTE)
@flask_admin.expose_plugview(UNKNOWN_ROUTE) # $ routeSetup
class WithUnknownRoute(MethodView):
def get(self, cls, maybeRouted): # $ requestHandler routedParameter=maybeRouted SPURIOUS: routedParameter=cls
def get(self, cls, maybeRouted): # $ requestHandler routedParameter=maybeRouted
ensure_tainted(maybeRouted) # $ tainted
ensure_not_tainted(cls) # $ SPURIOUS: tainted
return "ok"
ensure_not_tainted(cls)
return "ok" # $ HttpResponse
@app.route('/') # $ routeSetup="/"