Python: Add basic FastAPI support

This commit is contained in:
Rasmus Wriedt Larsen
2021-09-12 15:50:36 +02:00
parent f48c418d6d
commit 3661ff3bd8
10 changed files with 169 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
lgtm,codescanning
* Added modeling of sources/sinks when using FastAPI to create web servers.

View File

@@ -12,6 +12,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

View File

@@ -0,0 +1,75 @@
/**
* 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
/**
* 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() }
}
// ---------------------------------------------------------------------------
// 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() { this = App::instance().getMember(any(HTTP::httpVerbLower())).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(_)
}
override DataFlow::Node getUrlPatternArg() {
result in [this.getArg(0), this.getArgByName("path")]
}
override Function getARequestHandler() { result.getADecorator().getAFlowNode() = node }
override string getFramework() { result = "FastAPI" }
}
// ---------------------------------------------------------------------------
// Response modeling
// ---------------------------------------------------------------------------
/**
* Implicit response from returns of FastAPI request handlers
*/
private class FastApiRequestHandlerReturn extends HTTP::Server::HttpResponse::Range,
DataFlow::CfgNode {
FastApiRequestHandlerReturn() {
exists(Function requestHandler |
requestHandler = any(FastApiRouteSetup rs).getARequestHandler() and
node = requestHandler.getAReturnValueFlowNode()
)
}
override DataFlow::Node getBody() { result = this }
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
override string getMimetypeDefault() { result = "application/json" }
}
}

View File

@@ -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" }
}

View File

@@ -0,0 +1,3 @@
argumentToEnsureNotTaintedNotMarkedAsSpurious
untaintedArgumentToEnsureTaintedNotMarkedAsMissing
failures

View File

@@ -0,0 +1 @@
import experimental.meta.InlineTaintTest

View File

@@ -0,0 +1,73 @@
# 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
# Docs:
# see https://fastapi.tiangolo.com/tutorial/path-params/
# More stuff that we should support:
# - https://fastapi.tiangolo.com/tutorial/bigger-applications/
# - https://fastapi.tiangolo.com/advanced/response-cookies/
# - https://fastapi.tiangolo.com/tutorial/dependencies/
# - Extra taint-steps for files
# - https://fastapi.tiangolo.com/tutorial/request-files/
# - https://fastapi.tiangolo.com/tutorial/request-files/#uploadfile
# - https://fastapi.tiangolo.com/tutorial/background-tasks/
#
# - https://fastapi.tiangolo.com/tutorial/middleware/
# - https://fastapi.tiangolo.com/tutorial/encoder/
#
# - `app.route()`

View File

@@ -0,0 +1 @@
# TODO: Add detailed tests of ways to create responses in this file.