mirror of
https://github.com/github/codeql.git
synced 2026-04-30 11:15:13 +02:00
2
python/change-notes/2021-10-25-add-FastAPI-modeling.md
Normal file
2
python/change-notes/2021-10-25-add-FastAPI-modeling.md
Normal file
@@ -0,0 +1,2 @@
|
||||
lgtm,codescanning
|
||||
* Added modeling of sources/sinks when using FastAPI to create web servers.
|
||||
@@ -13,6 +13,7 @@ private import semmle.python.frameworks.Cryptography
|
||||
private import semmle.python.frameworks.Dill
|
||||
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.FlaskSqlAlchemy
|
||||
private import semmle.python.frameworks.Idna
|
||||
@@ -24,11 +25,13 @@ private import semmle.python.frameworks.Mysql
|
||||
private import semmle.python.frameworks.MySQLdb
|
||||
private import semmle.python.frameworks.Peewee
|
||||
private import semmle.python.frameworks.Psycopg2
|
||||
private import semmle.python.frameworks.Pydantic
|
||||
private import semmle.python.frameworks.PyMySQL
|
||||
private import semmle.python.frameworks.Rsa
|
||||
private import semmle.python.frameworks.RuamelYaml
|
||||
private import semmle.python.frameworks.Simplejson
|
||||
private import semmle.python.frameworks.SqlAlchemy
|
||||
private import semmle.python.frameworks.Starlette
|
||||
private import semmle.python.frameworks.Stdlib
|
||||
private import semmle.python.frameworks.Tornado
|
||||
private import semmle.python.frameworks.Twisted
|
||||
|
||||
352
python/ql/lib/semmle/python/frameworks/FastApi.qll
Normal file
352
python/ql/lib/semmle/python/frameworks/FastApi.qll
Normal file
@@ -0,0 +1,352 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `fastapi` PyPI package.
|
||||
* See https://fastapi.tiangolo.com/.
|
||||
*/
|
||||
|
||||
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.ApiGraphs
|
||||
private import semmle.python.frameworks.Pydantic
|
||||
private import semmle.python.frameworks.Starlette
|
||||
|
||||
/**
|
||||
* Provides models for the `fastapi` PyPI package.
|
||||
* See https://fastapi.tiangolo.com/.
|
||||
*/
|
||||
private module FastApi {
|
||||
/**
|
||||
* Provides models for FastAPI applications (an instance of `fastapi.FastAPI`).
|
||||
*/
|
||||
module App {
|
||||
/** Gets a reference to a FastAPI application (an instance of `fastapi.FastAPI`). */
|
||||
API::Node instance() { result = API::moduleImport("fastapi").getMember("FastAPI").getReturn() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides models for the `fastapi.APIRouter` class
|
||||
*
|
||||
* See https://fastapi.tiangolo.com/tutorial/bigger-applications/.
|
||||
*/
|
||||
module APIRouter {
|
||||
/** Gets a reference to an instance of `fastapi.APIRouter`. */
|
||||
API::Node instance() {
|
||||
result = API::moduleImport("fastapi").getMember("APIRouter").getReturn()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// routing modeling
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* A call to a method like `get` or `post` on a FastAPI application.
|
||||
*
|
||||
* See https://fastapi.tiangolo.com/tutorial/first-steps/#define-a-path-operation-decorator
|
||||
*/
|
||||
private class FastApiRouteSetup extends HTTP::Server::RouteSetup::Range, DataFlow::CallCfgNode {
|
||||
FastApiRouteSetup() {
|
||||
exists(string routeAddingMethod |
|
||||
routeAddingMethod = HTTP::httpVerbLower()
|
||||
or
|
||||
routeAddingMethod in ["api_route", "websocket"]
|
||||
|
|
||||
this = App::instance().getMember(routeAddingMethod).getACall()
|
||||
or
|
||||
this = APIRouter::instance().getMember(routeAddingMethod).getACall()
|
||||
)
|
||||
}
|
||||
|
||||
override Parameter getARoutedParameter() {
|
||||
// this will need to be refined a bit, since you can add special parameters to
|
||||
// your request handler functions that are used to pass in the response. There
|
||||
// might be other special cases as well, but as a start this is not too far off
|
||||
// the mark.
|
||||
result = this.getARequestHandler().getArgByName(_) and
|
||||
// type-annotated with `Response`
|
||||
not any(Response::RequestHandlerParam src).asExpr() = result
|
||||
}
|
||||
|
||||
override DataFlow::Node getUrlPatternArg() {
|
||||
result in [this.getArg(0), this.getArgByName("path")]
|
||||
}
|
||||
|
||||
override Function getARequestHandler() { result.getADecorator().getAFlowNode() = node }
|
||||
|
||||
override string getFramework() { result = "FastAPI" }
|
||||
|
||||
/** Gets the argument specifying the response class to use, if any. */
|
||||
DataFlow::Node getResponseClassArg() { result = this.getArgByName("response_class") }
|
||||
}
|
||||
|
||||
/**
|
||||
* A parameter to a request handler that has a type-annotation with a class that is a
|
||||
* Pydantic model.
|
||||
*/
|
||||
private class PydanticModelRequestHandlerParam extends Pydantic::BaseModel::InstanceSource,
|
||||
DataFlow::ParameterNode {
|
||||
PydanticModelRequestHandlerParam() {
|
||||
this.getParameter().getAnnotation() = Pydantic::BaseModel::subclassRef().getAUse().asExpr() and
|
||||
any(FastApiRouteSetup rs).getARequestHandler().getArgByName(_) = this.getParameter()
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Response modeling
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* A parameter to a request handler that has a WebSocket type-annotation.
|
||||
*/
|
||||
private class WebSocketRequestHandlerParam extends Starlette::WebSocket::InstanceSource,
|
||||
DataFlow::ParameterNode {
|
||||
WebSocketRequestHandlerParam() {
|
||||
this.getParameter().getAnnotation() = Starlette::WebSocket::classRef().getAUse().asExpr() and
|
||||
any(FastApiRouteSetup rs).getARequestHandler().getArgByName(_) = this.getParameter()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides models for the `fastapi.Response` class and subclasses.
|
||||
*
|
||||
* See https://fastapi.tiangolo.com/advanced/custom-response/#response.
|
||||
*/
|
||||
module Response {
|
||||
/**
|
||||
* Gets the `API::Node` for the manually modeled response classes called `name`.
|
||||
*/
|
||||
private API::Node getModeledResponseClass(string name) {
|
||||
name = "Response" and
|
||||
result = API::moduleImport("fastapi").getMember(name)
|
||||
or
|
||||
// see https://github.com/tiangolo/fastapi/blob/master/fastapi/responses.py
|
||||
name in [
|
||||
"Response", "HTMLResponse", "PlainTextResponse", "JSONResponse", "UJSONResponse",
|
||||
"ORJSONResponse", "RedirectResponse", "StreamingResponse", "FileResponse"
|
||||
] and
|
||||
result = API::moduleImport("fastapi").getMember("responses").getMember(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default MIME type for a FastAPI response class (defined with the
|
||||
* `media_type` class-attribute).
|
||||
*
|
||||
* Also models user-defined classes and tries to take inheritance into account.
|
||||
*
|
||||
* TODO: build easy way to solve problems like this, like we used to have the
|
||||
* `ClassValue.lookup` predicate.
|
||||
*/
|
||||
private string getDefaultMimeType(API::Node responseClass) {
|
||||
exists(string name | responseClass = getModeledResponseClass(name) |
|
||||
// no defaults for these.
|
||||
name in ["Response", "RedirectResponse", "StreamingResponse"] and
|
||||
none()
|
||||
or
|
||||
// For `FileResponse` the code will guess what mimetype
|
||||
// to use, or fall back to "text/plain", but claiming that all responses will
|
||||
// have "text/plain" per default is also highly inaccurate, so just going to not
|
||||
// do anything about this.
|
||||
name = "FileResponse" and
|
||||
none()
|
||||
or
|
||||
name = "HTMLResponse" and
|
||||
result = "text/html"
|
||||
or
|
||||
name = "PlainTextResponse" and
|
||||
result = "text/plain"
|
||||
or
|
||||
name in ["JSONResponse", "UJSONResponse", "ORJSONResponse"] and
|
||||
result = "application/json"
|
||||
)
|
||||
or
|
||||
// user-defined subclasses
|
||||
exists(Class cls, API::Node base |
|
||||
base = getModeledResponseClass(_).getASubclass*() and
|
||||
cls.getABase() = base.getAUse().asExpr() and
|
||||
responseClass.getAnImmediateUse().asExpr().(ClassExpr) = cls.getParent()
|
||||
|
|
||||
exists(Assign assign | assign = cls.getAStmt() |
|
||||
assign.getATarget().(Name).getId() = "media_type" and
|
||||
result = assign.getValue().(StrConst).getText()
|
||||
)
|
||||
or
|
||||
// TODO: this should use a proper MRO calculation instead
|
||||
not exists(Assign assign | assign = cls.getAStmt() |
|
||||
assign.getATarget().(Name).getId() = "media_type"
|
||||
) and
|
||||
result = getDefaultMimeType(base)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of instances of `fastapi.Response` and its' subclasses, extend this class to model new instances.
|
||||
*
|
||||
* This can include instantiations of the class, return values from function
|
||||
* calls, or a special parameter that will be set when functions are called by an external
|
||||
* library.
|
||||
*
|
||||
* Use the predicate `Response::instance()` to get references to instances of `fastapi.Response`.
|
||||
*/
|
||||
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
|
||||
|
||||
/** A direct instantiation of a response class. */
|
||||
private class ResponseInstantiation extends InstanceSource, HTTP::Server::HttpResponse::Range,
|
||||
DataFlow::CallCfgNode {
|
||||
API::Node baseApiNode;
|
||||
API::Node responseClass;
|
||||
|
||||
ResponseInstantiation() {
|
||||
baseApiNode = getModeledResponseClass(_) and
|
||||
responseClass = baseApiNode.getASubclass*() and
|
||||
this = responseClass.getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getBody() {
|
||||
not baseApiNode = getModeledResponseClass(["RedirectResponse", "FileResponse"]) and
|
||||
result in [this.getArg(0), this.getArgByName("content")]
|
||||
}
|
||||
|
||||
override DataFlow::Node getMimetypeOrContentTypeArg() {
|
||||
not baseApiNode = getModeledResponseClass("RedirectResponse") and
|
||||
result in [this.getArg(3), this.getArgByName("media_type")]
|
||||
}
|
||||
|
||||
override string getMimetypeDefault() { result = getDefaultMimeType(responseClass) }
|
||||
}
|
||||
|
||||
/**
|
||||
* A direct instantiation of a redirect response.
|
||||
*/
|
||||
private class RedirectResponseInstantiation extends ResponseInstantiation,
|
||||
HTTP::Server::HttpRedirectResponse::Range {
|
||||
RedirectResponseInstantiation() { baseApiNode = getModeledResponseClass("RedirectResponse") }
|
||||
|
||||
override DataFlow::Node getRedirectLocation() {
|
||||
result in [this.getArg(0), this.getArgByName("url")]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An implicit response from a return of FastAPI request handler.
|
||||
*/
|
||||
private class FastApiRequestHandlerReturn extends HTTP::Server::HttpResponse::Range,
|
||||
DataFlow::CfgNode {
|
||||
FastApiRouteSetup routeSetup;
|
||||
|
||||
FastApiRequestHandlerReturn() {
|
||||
node = routeSetup.getARequestHandler().getAReturnValueFlowNode()
|
||||
}
|
||||
|
||||
override DataFlow::Node getBody() { result = this }
|
||||
|
||||
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
|
||||
|
||||
override string getMimetypeDefault() {
|
||||
exists(API::Node responseClass |
|
||||
responseClass.getAUse() = routeSetup.getResponseClassArg() and
|
||||
result = getDefaultMimeType(responseClass)
|
||||
)
|
||||
or
|
||||
not exists(routeSetup.getResponseClassArg()) and
|
||||
result = "application/json"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An implicit response from a return of FastAPI request handler, that has
|
||||
* `response_class` set to a `FileResponse`.
|
||||
*/
|
||||
private class FastApiRequestHandlerFileResponseReturn extends FastApiRequestHandlerReturn {
|
||||
FastApiRequestHandlerFileResponseReturn() {
|
||||
exists(API::Node responseClass |
|
||||
responseClass.getAUse() = routeSetup.getResponseClassArg() and
|
||||
responseClass = getModeledResponseClass("FileResponse").getASubclass*()
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getBody() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
* An implicit response from a return of FastAPI request handler, that has
|
||||
* `response_class` set to a `RedirectResponse`.
|
||||
*/
|
||||
private class FastApiRequestHandlerRedirectReturn extends FastApiRequestHandlerReturn,
|
||||
HTTP::Server::HttpRedirectResponse::Range {
|
||||
FastApiRequestHandlerRedirectReturn() {
|
||||
exists(API::Node responseClass |
|
||||
responseClass.getAUse() = routeSetup.getResponseClassArg() and
|
||||
responseClass = getModeledResponseClass("RedirectResponse").getASubclass*()
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getBody() { none() }
|
||||
|
||||
override DataFlow::Node getRedirectLocation() { result = this }
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL: Do not use.
|
||||
*
|
||||
* A parameter to a FastAPI request-handler that has a `fastapi.Response`
|
||||
* type-annotation.
|
||||
*/
|
||||
class RequestHandlerParam extends InstanceSource, DataFlow::ParameterNode {
|
||||
RequestHandlerParam() {
|
||||
this.getParameter().getAnnotation() =
|
||||
getModeledResponseClass(_).getASubclass*().getAUse().asExpr() and
|
||||
any(FastApiRouteSetup rs).getARequestHandler().getArgByName(_) = this.getParameter()
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `fastapi.Response`. */
|
||||
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result instanceof InstanceSource
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `fastapi.Response`. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
/**
|
||||
* A call to `set_cookie` on a FastAPI Response.
|
||||
*/
|
||||
private class SetCookieCall extends HTTP::Server::CookieWrite::Range, DataFlow::MethodCallNode {
|
||||
SetCookieCall() { this.calls(instance(), "set_cookie") }
|
||||
|
||||
override DataFlow::Node getHeaderArg() { none() }
|
||||
|
||||
override DataFlow::Node getNameArg() { result in [this.getArg(0), this.getArgByName("key")] }
|
||||
|
||||
override DataFlow::Node getValueArg() {
|
||||
result in [this.getArg(1), this.getArgByName("value")]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to `append` on a `headers` of a FastAPI Response, with the `Set-Cookie`
|
||||
* header-key.
|
||||
*/
|
||||
private class HeadersAppendCookie extends HTTP::Server::CookieWrite::Range,
|
||||
DataFlow::MethodCallNode {
|
||||
HeadersAppendCookie() {
|
||||
exists(DataFlow::AttrRead headers, DataFlow::Node keyArg |
|
||||
headers.accesses(instance(), "headers") and
|
||||
this.calls(headers, "append") and
|
||||
keyArg in [this.getArg(0), this.getArgByName("key")] and
|
||||
keyArg.getALocalSource().asExpr().(StrConst).getText().toLowerCase() = "set-cookie"
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getHeaderArg() {
|
||||
result in [this.getArg(1), this.getArgByName("value")]
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { none() }
|
||||
|
||||
override DataFlow::Node getValueArg() { none() }
|
||||
}
|
||||
}
|
||||
}
|
||||
108
python/ql/lib/semmle/python/frameworks/Pydantic.qll
Normal file
108
python/ql/lib/semmle/python/frameworks/Pydantic.qll
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `pydantic` PyPI package.
|
||||
*
|
||||
* See
|
||||
* - https://pypi.org/project/pydantic/
|
||||
* - https://pydantic-docs.helpmanual.io/
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.dataflow.new.TaintTracking
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
|
||||
/**
|
||||
* INTERNAL: Do not use.
|
||||
*
|
||||
* Provides models for `pydantic` PyPI package.
|
||||
*
|
||||
* See
|
||||
* - https://pypi.org/project/pydantic/
|
||||
* - https://pydantic-docs.helpmanual.io/
|
||||
*/
|
||||
module Pydantic {
|
||||
/**
|
||||
* Provides models for `pydantic.BaseModel` subclasses (a pydantic model).
|
||||
*
|
||||
* See https://pydantic-docs.helpmanual.io/usage/models/.
|
||||
*/
|
||||
module BaseModel {
|
||||
/** Gets a reference to a `pydantic.BaseModel` subclass (a pydantic model). */
|
||||
API::Node subclassRef() {
|
||||
result = API::moduleImport("pydantic").getMember("BaseModel").getASubclass+()
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of instances of `pydantic.BaseModel` subclasses, extend this class to model new instances.
|
||||
*
|
||||
* This can include instantiations of the class, return values from function
|
||||
* calls, or a special parameter that will be set when functions are called by an external
|
||||
* library.
|
||||
*
|
||||
* Use the predicate `BaseModel::instance()` to get references to instances of `pydantic.BaseModel`.
|
||||
*/
|
||||
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
|
||||
|
||||
/** Gets a reference to an instance of a `pydantic.BaseModel` subclass. */
|
||||
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result instanceof InstanceSource
|
||||
or
|
||||
t.start() and
|
||||
instanceStepToPydanticModel(_, result)
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of a `pydantic.BaseModel` subclass. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
/**
|
||||
* A step from an instance of a `pydantic.BaseModel` subclass, that might result in
|
||||
* an instance of a `pydantic.BaseModel` subclass.
|
||||
*
|
||||
* NOTE: We currently overapproximate, and treat all attributes as containing
|
||||
* another pydantic model. For the code below, we _could_ limit this to `main_foo`
|
||||
* and members of `other_foos`. IF THIS IS CHANGED, YOU MUST CHANGE THE ADDITIONAL
|
||||
* TAINT STEPS BELOW, SUCH THAT SIMPLE ACCESS OF SOMETHIGN LIKE `str` IS STILL
|
||||
* TAINTED.
|
||||
*
|
||||
*
|
||||
* ```py
|
||||
* class MyComplexModel(BaseModel):
|
||||
* field: str
|
||||
* main_foo: Foo
|
||||
* other_foos: List[Foo]
|
||||
* ```
|
||||
*/
|
||||
private predicate instanceStepToPydanticModel(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
|
||||
// attributes (such as `model.foo`)
|
||||
nodeFrom = instance() and
|
||||
nodeTo.(DataFlow::AttrRead).getObject() = nodeFrom
|
||||
or
|
||||
// subscripts on attributes (such as `model.foo[0]`). This needs to handle nested
|
||||
// lists (such as `model.foo[0][0]`), and access being split into multiple
|
||||
// statements (such as `xs = model.foo; xs[0]`).
|
||||
//
|
||||
// To handle this we overapproximate which things are a Pydantic model, by
|
||||
// treating any subscript on anything that originates on a Pydantic model to also
|
||||
// be a Pydantic model. So `model[0]` will be an overapproximation, but should not
|
||||
// really cause problems (since we don't expect real code to contain such accesses)
|
||||
nodeFrom = instance() and
|
||||
nodeTo.asCfgNode().(SubscriptNode).getObject() = nodeFrom.asCfgNode()
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra taint propagation for `pydantic.BaseModel` subclasses. (note that these could also be `pydantic.BaseModel` subclasses)
|
||||
*/
|
||||
private class AdditionalTaintStep extends TaintTracking::AdditionalTaintStep {
|
||||
override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
|
||||
// NOTE: if `instanceStepToPydanticModel` is changed to be more precise, these
|
||||
// taint steps should be expanded, such that a field that has type `str` is
|
||||
// still tainted.
|
||||
instanceStepToPydanticModel(nodeFrom, nodeTo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
162
python/ql/lib/semmle/python/frameworks/Starlette.qll
Normal file
162
python/ql/lib/semmle/python/frameworks/Starlette.qll
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `starlette` PyPI package.
|
||||
*
|
||||
* See
|
||||
* - https://pypi.org/project/starlette/
|
||||
* - https://www.starlette.io/
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.dataflow.new.TaintTracking
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
private import semmle.python.frameworks.internal.InstanceTaintStepsHelper
|
||||
private import semmle.python.frameworks.Stdlib
|
||||
|
||||
/**
|
||||
* INTERNAL: Do not use.
|
||||
*
|
||||
* Provides models for `starlette` PyPI package.
|
||||
*
|
||||
* See
|
||||
* - https://pypi.org/project/starlette/
|
||||
* - https://www.starlette.io/
|
||||
*/
|
||||
module Starlette {
|
||||
/**
|
||||
* Provides models for the `starlette.websockets.WebSocket` class
|
||||
*
|
||||
* See https://www.starlette.io/websockets/.
|
||||
*/
|
||||
module WebSocket {
|
||||
/** Gets a reference to the `starlette.websockets.WebSocket` class. */
|
||||
API::Node classRef() {
|
||||
result = API::moduleImport("starlette").getMember("websockets").getMember("WebSocket")
|
||||
or
|
||||
result = API::moduleImport("fastapi").getMember("WebSocket")
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of instances of `starlette.websockets.WebSocket`, extend this class to model new instances.
|
||||
*
|
||||
* This can include instantiations of the class, return values from function
|
||||
* calls, or a special parameter that will be set when functions are called by an external
|
||||
* library.
|
||||
*
|
||||
* Use the predicate `WebSocket::instance()` to get references to instances of `starlette.websockets.WebSocket`.
|
||||
*/
|
||||
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
|
||||
|
||||
/** A direct instantiation of `starlette.websockets.WebSocket`. */
|
||||
private class ClassInstantiation extends InstanceSource, DataFlow::CallCfgNode {
|
||||
ClassInstantiation() { this = classRef().getACall() }
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `starlette.websockets.WebSocket`. */
|
||||
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result instanceof InstanceSource
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `starlette.websockets.WebSocket`. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
/**
|
||||
* Taint propagation for `starlette.websockets.WebSocket`.
|
||||
*/
|
||||
private class InstanceTaintSteps extends InstanceTaintStepsHelper {
|
||||
InstanceTaintSteps() { this = "starlette.websockets.WebSocket" }
|
||||
|
||||
override DataFlow::Node getInstance() { result = instance() }
|
||||
|
||||
override string getAttributeName() { result in ["url", "headers", "query_params", "cookies"] }
|
||||
|
||||
override string getMethodName() { none() }
|
||||
|
||||
override string getAsyncMethodName() {
|
||||
result in [
|
||||
"receive", "receive_bytes", "receive_text", "receive_json", "iter_bytes", "iter_text",
|
||||
"iter_json"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/** An attribute read on a `starlette.websockets.WebSocket` instance that is a `starlette.requests.URL` instance. */
|
||||
private class UrlInstances extends URL::InstanceSource {
|
||||
UrlInstances() {
|
||||
this.(DataFlow::AttrRead).getObject() = instance() and
|
||||
this.(DataFlow::AttrRead).getAttributeName() = "url"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides models for the `starlette.requests.URL` class
|
||||
*
|
||||
* See the URL part of https://www.starlette.io/websockets/.
|
||||
*/
|
||||
module URL {
|
||||
/** Gets a reference to the `starlette.requests.URL` class. */
|
||||
private API::Node classRef() {
|
||||
result = API::moduleImport("starlette").getMember("requests").getMember("URL")
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of instances of `starlette.requests.URL`, extend this class to model new instances.
|
||||
*
|
||||
* This can include instantiations of the class, return values from function
|
||||
* calls, or a special parameter that will be set when functions are called by an external
|
||||
* library.
|
||||
*
|
||||
* Use the predicate `URL::instance()` to get references to instances of `starlette.requests.URL`.
|
||||
*/
|
||||
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
|
||||
|
||||
/** A direct instantiation of `starlette.requests.URL`. */
|
||||
private class ClassInstantiation extends InstanceSource, DataFlow::CallCfgNode {
|
||||
ClassInstantiation() { this = classRef().getACall() }
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `starlette.requests.URL`. */
|
||||
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result instanceof InstanceSource
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `starlette.requests.URL`. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
/**
|
||||
* Taint propagation for `starlette.requests.URL`.
|
||||
*/
|
||||
private class InstanceTaintSteps extends InstanceTaintStepsHelper {
|
||||
InstanceTaintSteps() { this = "starlette.requests.URL" }
|
||||
|
||||
override DataFlow::Node getInstance() { result = instance() }
|
||||
|
||||
override string getAttributeName() {
|
||||
result in [
|
||||
"components", "netloc", "path", "query", "fragment", "username", "password", "hostname",
|
||||
"port"
|
||||
]
|
||||
}
|
||||
|
||||
override string getMethodName() { none() }
|
||||
|
||||
override string getAsyncMethodName() { none() }
|
||||
}
|
||||
|
||||
/** An attribute read on a `starlette.requests.URL` instance that is a `urllib.parse.SplitResult` instance. */
|
||||
private class UrlSplitInstances extends Stdlib::SplitResult::InstanceSource instanceof DataFlow::AttrRead {
|
||||
UrlSplitInstances() {
|
||||
super.getObject() = instance() and
|
||||
super.getAttributeName() = "components"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,6 +167,74 @@ module Stdlib {
|
||||
override string getAsyncMethodName() { none() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides models for the `urllib.parse.SplitResult` class
|
||||
*
|
||||
* See https://docs.python.org/3.9/library/urllib.parse.html#urllib.parse.SplitResult.
|
||||
*/
|
||||
module SplitResult {
|
||||
/** Gets a reference to the `urllib.parse.SplitResult` class. */
|
||||
private API::Node classRef() {
|
||||
result = API::moduleImport("urllib").getMember("parse").getMember("SplitResult")
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of instances of `urllib.parse.SplitResult`, extend this class to model new instances.
|
||||
*
|
||||
* This can include instantiations of the class, return values from function
|
||||
* calls, or a special parameter that will be set when functions are called by an external
|
||||
* library.
|
||||
*
|
||||
* Use the predicate `SplitResult::instance()` to get references to instances of `urllib.parse.SplitResult`.
|
||||
*/
|
||||
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
|
||||
|
||||
/** A direct instantiation of `urllib.parse.SplitResult`. */
|
||||
private class ClassInstantiation extends InstanceSource, DataFlow::CallCfgNode {
|
||||
ClassInstantiation() { this = classRef().getACall() }
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `urllib.parse.SplitResult`. */
|
||||
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result instanceof InstanceSource
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `urllib.parse.SplitResult`. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
/**
|
||||
* Taint propagation for `urllib.parse.SplitResult`.
|
||||
*/
|
||||
private class InstanceTaintSteps extends InstanceTaintStepsHelper {
|
||||
InstanceTaintSteps() { this = "urllib.parse.SplitResult" }
|
||||
|
||||
override DataFlow::Node getInstance() { result = instance() }
|
||||
|
||||
override string getAttributeName() {
|
||||
result in [
|
||||
"netloc", "path", "query", "fragment", "username", "password", "hostname", "port"
|
||||
]
|
||||
}
|
||||
|
||||
override string getMethodName() { none() }
|
||||
|
||||
override string getAsyncMethodName() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Extra taint propagation for `urllib.parse.SplitResult`, not covered by `InstanceTaintSteps`.
|
||||
*/
|
||||
private class AdditionalTaintStep extends TaintTracking::AdditionalTaintStep {
|
||||
override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
|
||||
// TODO
|
||||
none()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1749,6 +1817,30 @@ private module StdlibPrivate {
|
||||
|
||||
override string getKind() { result = Escaping::getRegexKind() }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// urllib
|
||||
// ---------------------------------------------------------------------------
|
||||
/**
|
||||
* A call to `urllib.parse.urlsplit`
|
||||
*
|
||||
* See https://docs.python.org/3.9/library/urllib.parse.html#urllib.parse.urlsplit
|
||||
*/
|
||||
class UrllibParseUrlsplitCall extends Stdlib::SplitResult::InstanceSource, DataFlow::CallCfgNode {
|
||||
UrllibParseUrlsplitCall() {
|
||||
this = API::moduleImport("urllib").getMember("parse").getMember("urlsplit").getACall()
|
||||
}
|
||||
|
||||
/** Gets the argument that specifies the URL. */
|
||||
DataFlow::Node getUrl() { result in [this.getArg(0), this.getArgByName("url")] }
|
||||
}
|
||||
|
||||
/** Extra taint-step such that the result of `urllib.parse.urlsplit(tainted_string)` is tainted. */
|
||||
private class UrllibParseUrlsplitCallAdditionalTaintStep extends TaintTracking::AdditionalTaintStep {
|
||||
override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
|
||||
nodeTo.(UrllibParseUrlsplitCall).getUrl() = nodeFrom
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import python
|
||||
import experimental.meta.ConceptsTest
|
||||
|
||||
class DedicatedResponseTest extends HttpServerHttpResponseTest {
|
||||
DedicatedResponseTest() { file.getShortName() = "response_test.py" }
|
||||
}
|
||||
|
||||
class OtherResponseTest extends HttpServerHttpResponseTest {
|
||||
OtherResponseTest() { not this instanceof DedicatedResponseTest }
|
||||
|
||||
override string getARelevantTag() { result = "HttpResponse" }
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
argumentToEnsureNotTaintedNotMarkedAsSpurious
|
||||
untaintedArgumentToEnsureTaintedNotMarkedAsMissing
|
||||
failures
|
||||
@@ -0,0 +1 @@
|
||||
import experimental.meta.InlineTaintTest
|
||||
66
python/ql/test/library-tests/frameworks/fastapi/basic.py
Normal file
66
python/ql/test/library-tests/frameworks/fastapi/basic.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# Taking inspiration from https://realpython.com/fastapi-python-web-apis/
|
||||
|
||||
# run with
|
||||
# uvicorn basic:app --reload
|
||||
# Then visit http://127.0.0.1:8000/docs and http://127.0.0.1:8000/redoc
|
||||
|
||||
from fastapi import FastAPI
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
@app.get("/") # $ routeSetup="/"
|
||||
async def root(): # $ requestHandler
|
||||
return {"message": "Hello World"} # $ HttpResponse
|
||||
|
||||
@app.get("/non-async") # $ routeSetup="/non-async"
|
||||
def non_async(): # $ requestHandler
|
||||
return {"message": "non-async"} # $ HttpResponse
|
||||
|
||||
@app.get(path="/kw-arg") # $ routeSetup="/kw-arg"
|
||||
def kw_arg(): # $ requestHandler
|
||||
return {"message": "kw arg"} # $ HttpResponse
|
||||
|
||||
@app.get("/foo/{foo_id}") # $ routeSetup="/foo/{foo_id}"
|
||||
async def get_foo(foo_id: int): # $ requestHandler routedParameter=foo_id
|
||||
# FastAPI does data validation (with `pydantic` PyPI package) under the hood based
|
||||
# on the type annotation we did for `foo_id`, so it will auto-reject anything that's
|
||||
# not an int.
|
||||
return {"foo_id": foo_id} # $ HttpResponse
|
||||
|
||||
# this will work as query param, so `/bar?bar_id=123`
|
||||
@app.get("/bar") # $ routeSetup="/bar"
|
||||
async def get_bar(bar_id: int = 42): # $ requestHandler routedParameter=bar_id
|
||||
return {"bar_id": bar_id} # $ HttpResponse
|
||||
|
||||
# The big deal is that FastAPI works so well together with pydantic, so you can do stuff like this
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
class Item(BaseModel):
|
||||
name: str
|
||||
price: float
|
||||
is_offer: Optional[bool] = None
|
||||
|
||||
@app.post("/items/") # $ routeSetup="/items/"
|
||||
async def create_item(item: Item): # $ requestHandler routedParameter=item
|
||||
# Note: calling `item` a routed parameter is slightly untrue, since it doesn't come
|
||||
# from the URL itself, but from the body of the POST request
|
||||
return item # $ HttpResponse
|
||||
|
||||
# this also works fine
|
||||
@app.post("/2items") # $ routeSetup="/2items"
|
||||
async def create_item2(item1: Item, item2: Item): # $ requestHandler routedParameter=item1 routedParameter=item2
|
||||
return (item1, item2) # $ HttpResponse
|
||||
|
||||
@app.api_route("/baz/{baz_id}", methods=["GET"]) # $ routeSetup="/baz/{baz_id}"
|
||||
async def get_baz(baz_id: int): # $ requestHandler routedParameter=baz_id
|
||||
return {"baz_id2": baz_id} # $ HttpResponse
|
||||
|
||||
# Docs:
|
||||
# see https://fastapi.tiangolo.com/tutorial/path-params/
|
||||
|
||||
# Things we should look at supporting:
|
||||
# - https://fastapi.tiangolo.com/tutorial/dependencies/
|
||||
# - https://fastapi.tiangolo.com/tutorial/background-tasks/
|
||||
# - https://fastapi.tiangolo.com/tutorial/middleware/
|
||||
# - https://fastapi.tiangolo.com/tutorial/encoder/
|
||||
145
python/ql/test/library-tests/frameworks/fastapi/response_test.py
Normal file
145
python/ql/test/library-tests/frameworks/fastapi/response_test.py
Normal file
@@ -0,0 +1,145 @@
|
||||
# see https://fastapi.tiangolo.com/advanced/response-cookies/
|
||||
|
||||
from fastapi import FastAPI, Response
|
||||
import fastapi.responses
|
||||
import asyncio
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.get("/response_parameter") # $ routeSetup="/response_parameter"
|
||||
async def response_parameter(response: Response): # $ requestHandler
|
||||
response.set_cookie("key", "value") # $ CookieWrite CookieName="key" CookieValue="value"
|
||||
response.set_cookie(key="key", value="value") # $ CookieWrite CookieName="key" CookieValue="value"
|
||||
response.headers.append("Set-Cookie", "key2=value2") # $ CookieWrite CookieRawHeader="key2=value2"
|
||||
response.headers.append(key="Set-Cookie", value="key2=value2") # $ CookieWrite CookieRawHeader="key2=value2"
|
||||
response.headers["X-MyHeader"] = "header-value"
|
||||
response.status_code = 418
|
||||
return {"message": "response as parameter"} # $ HttpResponse mimetype=application/json responseBody=Dict
|
||||
|
||||
|
||||
@app.get("/resp_parameter") # $ routeSetup="/resp_parameter"
|
||||
async def resp_parameter(resp: Response): # $ requestHandler
|
||||
resp.status_code = 418
|
||||
return {"message": "resp as parameter"} # $ HttpResponse mimetype=application/json responseBody=Dict
|
||||
|
||||
|
||||
@app.get("/response_parameter_no_type") # $ routeSetup="/response_parameter_no_type"
|
||||
async def response_parameter_no_type(response): # $ requestHandler routedParameter=response
|
||||
# NOTE: This does in fact not work, since FastAPI relies on the type annotations,
|
||||
# and not on the name of the parameter
|
||||
response.status_code = 418
|
||||
return {"message": "response as parameter"} # $ HttpResponse mimetype=application/json responseBody=Dict
|
||||
|
||||
|
||||
class MyXmlResponse(fastapi.responses.Response):
|
||||
media_type = "application/xml"
|
||||
|
||||
|
||||
@app.get("/response_parameter_custom_type", response_class=MyXmlResponse) # $ routeSetup="/response_parameter_custom_type"
|
||||
async def response_parameter_custom_type(response: MyXmlResponse): # $ requestHandler
|
||||
# NOTE: This is a contrived example of using a wrong annotation for the response
|
||||
# parameter. It will be passed a `fastapi.responses.Response` value when handling an
|
||||
# incoming request, so NOT a `MyXmlResponse` value. Cookies/Headers are still
|
||||
# propagated to the final response though.
|
||||
print(type(response))
|
||||
assert type(response) == fastapi.responses.Response
|
||||
response.set_cookie("key", "value") # $ CookieWrite CookieName="key" CookieValue="value"
|
||||
response.headers["Custom-Response-Type"] = "yes, but only after function has run"
|
||||
xml_data = "<foo>FOO</foo>"
|
||||
return xml_data # $ HttpResponse responseBody=xml_data mimetype=application/xml
|
||||
|
||||
|
||||
# Direct response construction
|
||||
|
||||
# see https://fastapi.tiangolo.com/advanced/response-directly/
|
||||
# see https://fastapi.tiangolo.com/advanced/custom-response/
|
||||
|
||||
|
||||
|
||||
@app.get("/direct_response") # $ routeSetup="/direct_response"
|
||||
async def direct_response(): # $ requestHandler
|
||||
xml_data = "<foo>FOO</foo>"
|
||||
resp = fastapi.responses.Response(xml_data, 200, None, "application/xml") # $ HttpResponse mimetype=application/xml responseBody=xml_data
|
||||
resp = fastapi.responses.Response(content=xml_data, media_type="application/xml") # $ HttpResponse mimetype=application/xml responseBody=xml_data
|
||||
return resp # $ SPURIOUS: HttpResponse mimetype=application/json responseBody=resp
|
||||
|
||||
|
||||
@app.get("/direct_response2", response_class=fastapi.responses.Response) # $ routeSetup="/direct_response2"
|
||||
async def direct_response2(): # $ requestHandler
|
||||
xml_data = "<foo>FOO</foo>"
|
||||
return xml_data # $ HttpResponse responseBody=xml_data
|
||||
|
||||
|
||||
@app.get("/my_xml_response") # $ routeSetup="/my_xml_response"
|
||||
async def my_xml_response(): # $ requestHandler
|
||||
xml_data = "<foo>FOO</foo>"
|
||||
resp = MyXmlResponse(content=xml_data) # $ HttpResponse mimetype=application/xml responseBody=xml_data
|
||||
return resp # $ SPURIOUS: HttpResponse mimetype=application/json responseBody=resp
|
||||
|
||||
|
||||
@app.get("/my_xml_response2", response_class=MyXmlResponse) # $ routeSetup="/my_xml_response2"
|
||||
async def my_xml_response2(): # $ requestHandler
|
||||
xml_data = "<foo>FOO</foo>"
|
||||
return xml_data # $ HttpResponse responseBody=xml_data mimetype=application/xml
|
||||
|
||||
|
||||
@app.get("/html_response") # $ routeSetup="/html_response"
|
||||
async def html_response(): # $ requestHandler
|
||||
hello_world = "<h1>Hello World!</h1>"
|
||||
resp = fastapi.responses.HTMLResponse(hello_world) # $ HttpResponse mimetype=text/html responseBody=hello_world
|
||||
return resp # $ SPURIOUS: HttpResponse mimetype=application/json responseBody=resp
|
||||
|
||||
|
||||
@app.get("/html_response2", response_class=fastapi.responses.HTMLResponse) # $ routeSetup="/html_response2"
|
||||
async def html_response2(): # $ requestHandler
|
||||
hello_world = "<h1>Hello World!</h1>"
|
||||
return hello_world # $ HttpResponse responseBody=hello_world mimetype=text/html
|
||||
|
||||
|
||||
@app.get("/redirect") # $ routeSetup="/redirect"
|
||||
async def redirect(): # $ requestHandler
|
||||
next = "https://www.example.com"
|
||||
resp = fastapi.responses.RedirectResponse(next) # $ HttpResponse HttpRedirectResponse redirectLocation=next
|
||||
return resp # $ SPURIOUS: HttpResponse mimetype=application/json responseBody=resp
|
||||
|
||||
|
||||
@app.get("/redirect2", response_class=fastapi.responses.RedirectResponse) # $ routeSetup="/redirect2"
|
||||
async def redirect2(): # $ requestHandler
|
||||
next = "https://www.example.com"
|
||||
return next # $ HttpResponse HttpRedirectResponse redirectLocation=next
|
||||
|
||||
|
||||
@app.get("/streaming_response") # $ routeSetup="/streaming_response"
|
||||
async def streaming_response(): # $ requestHandler
|
||||
# You can test this with curl:
|
||||
# curl --no-buffer http://127.0.0.1:8000/streaming_response
|
||||
async def content():
|
||||
yield b"Hello "
|
||||
await asyncio.sleep(0.5)
|
||||
yield b"World"
|
||||
await asyncio.sleep(0.5)
|
||||
yield b"!"
|
||||
|
||||
resp = fastapi.responses.StreamingResponse(content()) # $ HttpResponse responseBody=content()
|
||||
return resp # $ SPURIOUS: HttpResponse mimetype=application/json responseBody=resp
|
||||
|
||||
|
||||
# setting `response_class` to `StreamingResponse` does not seem to work
|
||||
# so no such example here
|
||||
|
||||
|
||||
@app.get("/file_response") # $ routeSetup="/file_response"
|
||||
async def file_response(): # $ requestHandler
|
||||
# has internal dependency on PyPI package `aiofiles`
|
||||
# will guess MIME type from file extension
|
||||
|
||||
# We don't really have any good QL modeling of passing a file-path, whose content
|
||||
# will be returned as part of the response... so will leave this as a TODO for now.
|
||||
resp = fastapi.responses.FileResponse(__file__) # $ HttpResponse
|
||||
return resp # $ SPURIOUS: HttpResponse mimetype=application/json responseBody=resp
|
||||
|
||||
|
||||
@app.get("/file_response2", response_class=fastapi.responses.FileResponse) # $ routeSetup="/file_response2"
|
||||
async def file_response2(): # $ requestHandler
|
||||
return __file__ # $ HttpResponse
|
||||
33
python/ql/test/library-tests/frameworks/fastapi/router.py
Normal file
33
python/ql/test/library-tests/frameworks/fastapi/router.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# like blueprints in Flask
|
||||
# see https://fastapi.tiangolo.com/tutorial/bigger-applications/
|
||||
|
||||
from fastapi import APIRouter, FastAPI
|
||||
|
||||
|
||||
inner_router = APIRouter()
|
||||
|
||||
@inner_router.get("/foo") # $ routeSetup="/foo"
|
||||
async def root(): # $ requestHandler
|
||||
return {"msg": "inner_router /foo"} # $ HttpResponse
|
||||
|
||||
outer_router = APIRouter()
|
||||
outer_router.include_router(inner_router, prefix="/inner")
|
||||
|
||||
|
||||
items_router = APIRouter(
|
||||
prefix="/items",
|
||||
tags=["items"],
|
||||
)
|
||||
|
||||
|
||||
@items_router.get("/") # $ routeSetup="/"
|
||||
async def items(): # $ requestHandler
|
||||
return {"msg": "items_router /"} # $ HttpResponse
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
app.include_router(outer_router, prefix="/outer")
|
||||
app.include_router(items_router)
|
||||
|
||||
# see basic.py for instructions for how to run this code.
|
||||
189
python/ql/test/library-tests/frameworks/fastapi/taint_test.py
Normal file
189
python/ql/test/library-tests/frameworks/fastapi/taint_test.py
Normal file
@@ -0,0 +1,189 @@
|
||||
# --- to make things runable ---
|
||||
|
||||
ensure_tainted = ensure_not_tainted = print
|
||||
|
||||
# --- real code ---
|
||||
|
||||
from fastapi import FastAPI
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class Foo(BaseModel):
|
||||
foo: str
|
||||
|
||||
|
||||
class MyComplexModel(BaseModel):
|
||||
field: str
|
||||
main_foo: Foo
|
||||
other_foos: List[Foo]
|
||||
nested_foos: List[List[Foo]]
|
||||
|
||||
|
||||
@app.post("/test_taint/{name}/{number}") # $ routeSetup="/test_taint/{name}/{number}"
|
||||
async def test_taint(name : str, number : int, also_input: MyComplexModel): # $ requestHandler routedParameter=name routedParameter=number routedParameter=also_input
|
||||
ensure_tainted(
|
||||
name, # $ tainted
|
||||
number, # $ tainted
|
||||
|
||||
also_input, # $ tainted
|
||||
also_input.field, # $ tainted
|
||||
|
||||
also_input.main_foo, # $ tainted
|
||||
also_input.main_foo.foo, # $ tainted
|
||||
|
||||
also_input.other_foos, # $ tainted
|
||||
also_input.other_foos[0], # $ tainted
|
||||
also_input.other_foos[0].foo, # $ tainted
|
||||
[f.foo for f in also_input.other_foos], # $ MISSING: tainted
|
||||
|
||||
also_input.nested_foos, # $ tainted
|
||||
also_input.nested_foos[0], # $ tainted
|
||||
also_input.nested_foos[0][0], # $ tainted
|
||||
also_input.nested_foos[0][0].foo, # $ tainted
|
||||
)
|
||||
|
||||
other_foos = also_input.other_foos
|
||||
|
||||
ensure_tainted(
|
||||
other_foos, # $ tainted
|
||||
other_foos[0], # $ tainted
|
||||
other_foos[0].foo, # $ tainted
|
||||
[f.foo for f in other_foos], # $ MISSING: tainted
|
||||
)
|
||||
|
||||
return "ok" # $ HttpResponse
|
||||
|
||||
|
||||
# --- body ---
|
||||
# see https://fastapi.tiangolo.com/tutorial/body-multiple-params/
|
||||
|
||||
from fastapi import Body
|
||||
|
||||
# request is made such as `/will-be-query-param?name=foo`
|
||||
@app.post("/will-be-query-param") # $ routeSetup="/will-be-query-param"
|
||||
async def will_be_query_param(name: str): # $ requestHandler routedParameter=name
|
||||
ensure_tainted(name) # $ tainted
|
||||
return "ok" # $ HttpResponse
|
||||
|
||||
# with the `= Body(...)` "annotation" FastAPI will know to transmit `name` as part of
|
||||
# the HTTP post body
|
||||
@app.post("/will-not-be-query-param") # $ routeSetup="/will-not-be-query-param"
|
||||
async def will_not_be_query_param(name: str = Body("foo", media_type="text/plain")): # $ requestHandler routedParameter=name
|
||||
ensure_tainted(name) # $ tainted
|
||||
return "ok" # $ HttpResponse
|
||||
|
||||
|
||||
# --- form data ---
|
||||
# see https://fastapi.tiangolo.com/tutorial/request-forms/
|
||||
|
||||
from fastapi import Form
|
||||
|
||||
@app.post("/form-example") # $ routeSetup="/form-example"
|
||||
async def form_example(username: str = Form(None)): # $ requestHandler routedParameter=username
|
||||
ensure_tainted(username) # $ tainted
|
||||
return "ok" # $ HttpResponse
|
||||
|
||||
|
||||
# --- HTTP headers ---
|
||||
# see https://fastapi.tiangolo.com/tutorial/header-params/
|
||||
|
||||
from fastapi import Header
|
||||
|
||||
@app.get("/header-example") # $ routeSetup="/header-example"
|
||||
async def header_example(user_agent: Optional[str] = Header(None)): # $ requestHandler routedParameter=user_agent
|
||||
ensure_tainted(user_agent) # $ tainted
|
||||
return "ok" # $ HttpResponse
|
||||
|
||||
|
||||
# --- file upload ---
|
||||
# see https://fastapi.tiangolo.com/tutorial/request-files/
|
||||
# see https://fastapi.tiangolo.com/tutorial/request-files/#uploadfile
|
||||
|
||||
from fastapi import File, UploadFile
|
||||
|
||||
@app.post("/file-upload") # $ routeSetup="/file-upload"
|
||||
async def file_upload(f1: bytes = File(None), f2: UploadFile = File(None)): # $ requestHandler routedParameter=f1 routedParameter=f2
|
||||
ensure_tainted(
|
||||
f1, # $ tainted
|
||||
|
||||
f2, # $ tainted
|
||||
f2.filename, # $ MISSING: tainted
|
||||
f2.content_type, # $ MISSING: tainted
|
||||
f2.file, # $ MISSING: tainted
|
||||
f2.file.read(), # $ MISSING: tainted
|
||||
f2.file.readline(), # $ MISSING: tainted
|
||||
f2.file.readlines(), # $ MISSING: tainted
|
||||
await f2.read(), # $ MISSING: tainted
|
||||
)
|
||||
return "ok" # $ HttpResponse
|
||||
|
||||
# --- WebSocket ---
|
||||
|
||||
import starlette.websockets
|
||||
from fastapi import WebSocket
|
||||
|
||||
|
||||
assert WebSocket == starlette.websockets.WebSocket
|
||||
|
||||
|
||||
@app.websocket("/ws") # $ routeSetup="/ws"
|
||||
async def websocket_test(websocket: WebSocket): # $ requestHandler routedParameter=websocket
|
||||
await websocket.accept()
|
||||
|
||||
ensure_tainted(
|
||||
websocket, # $ tainted
|
||||
|
||||
websocket.url, # $ tainted
|
||||
|
||||
websocket.url.netloc, # $ tainted
|
||||
websocket.url.path, # $ tainted
|
||||
websocket.url.query, # $ tainted
|
||||
websocket.url.fragment, # $ tainted
|
||||
websocket.url.username, # $ tainted
|
||||
websocket.url.password, # $ tainted
|
||||
websocket.url.hostname, # $ tainted
|
||||
websocket.url.port, # $ tainted
|
||||
|
||||
websocket.url.components, # $ tainted
|
||||
websocket.url.components.netloc, # $ tainted
|
||||
websocket.url.components.path, # $ tainted
|
||||
websocket.url.components.query, # $ tainted
|
||||
websocket.url.components.fragment, # $ tainted
|
||||
websocket.url.components.username, # $ tainted
|
||||
websocket.url.components.password, # $ tainted
|
||||
websocket.url.components.hostname, # $ tainted
|
||||
websocket.url.components.port, # $ tainted
|
||||
|
||||
websocket.headers, # $ tainted
|
||||
websocket.headers["key"], # $ tainted
|
||||
|
||||
websocket.query_params, # $ tainted
|
||||
websocket.query_params["key"], # $ tainted
|
||||
|
||||
websocket.cookies, # $ tainted
|
||||
websocket.cookies["key"], # $ tainted
|
||||
|
||||
await websocket.receive(), # $ tainted
|
||||
await websocket.receive_bytes(), # $ tainted
|
||||
await websocket.receive_text(), # $ tainted
|
||||
await websocket.receive_json(), # $ tainted
|
||||
)
|
||||
|
||||
# scheme seems very unlikely to give interesting results, but very likely to give FPs.
|
||||
ensure_not_tainted(
|
||||
websocket.url.scheme,
|
||||
websocket.url.components.scheme,
|
||||
)
|
||||
|
||||
async for data in websocket.iter_bytes():
|
||||
ensure_tainted(data) # $ tainted
|
||||
|
||||
async for data in websocket.iter_text():
|
||||
ensure_tainted(data) # $ tainted
|
||||
|
||||
async for data in websocket.iter_json():
|
||||
ensure_tainted(data) # $ tainted
|
||||
Reference in New Issue
Block a user