Skip to content

Commit f10f053

Browse files
authored
Merge pull request #7228 from RasmusWL/fastapi-improvements
Python: FastAPI improvements
2 parents 4609b20 + 1411804 commit f10f053

File tree

5 files changed

+43
-5
lines changed

5 files changed

+43
-5
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lgtm,codescanning
2+
* Extended the modeling of FastAPI such that custom subclasses of `fastapi.APIRouter` are recognized.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lgtm,codescanning
2+
* Extended the modeling of FastAPI such that `fastapi.responses.FileResponse` are considered `FileSystemAccess`, making them sinks for the _Uncontrolled data used in path expression_ (`py/path-injection`) query.

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ private module FastApi {
3333
module APIRouter {
3434
/** Gets a reference to an instance of `fastapi.APIRouter`. */
3535
API::Node instance() {
36-
result = API::moduleImport("fastapi").getMember("APIRouter").getReturn()
36+
result = API::moduleImport("fastapi").getMember("APIRouter").getASubclass*().getReturn()
3737
}
3838
}
3939

@@ -226,6 +226,17 @@ private module FastApi {
226226
}
227227
}
228228

229+
/**
230+
* A direct instantiation of a FileResponse.
231+
*/
232+
private class FileResponseInstantiation extends ResponseInstantiation, FileSystemAccess::Range {
233+
FileResponseInstantiation() { baseApiNode = getModeledResponseClass("FileResponse") }
234+
235+
override DataFlow::Node getAPathArgument() {
236+
result in [this.getArg(0), this.getArgByName("path")]
237+
}
238+
}
239+
229240
/**
230241
* An implicit response from a return of FastAPI request handler.
231242
*/
@@ -256,7 +267,8 @@ private module FastApi {
256267
* An implicit response from a return of FastAPI request handler, that has
257268
* `response_class` set to a `FileResponse`.
258269
*/
259-
private class FastApiRequestHandlerFileResponseReturn extends FastApiRequestHandlerReturn {
270+
private class FastApiRequestHandlerFileResponseReturn extends FastApiRequestHandlerReturn,
271+
FileSystemAccess::Range {
260272
FastApiRequestHandlerFileResponseReturn() {
261273
exists(API::Node responseClass |
262274
responseClass.getAUse() = routeSetup.getResponseClassArg() and
@@ -265,6 +277,8 @@ private module FastApi {
265277
}
266278

267279
override DataFlow::Node getBody() { none() }
280+
281+
override DataFlow::Node getAPathArgument() { result = this }
268282
}
269283

270284
/**

python/ql/test/library-tests/frameworks/fastapi/response_test.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,10 @@ async def file_response(): # $ requestHandler
136136

137137
# We don't really have any good QL modeling of passing a file-path, whose content
138138
# will be returned as part of the response... so will leave this as a TODO for now.
139-
resp = fastapi.responses.FileResponse(__file__) # $ HttpResponse
139+
resp = fastapi.responses.FileResponse(__file__) # $ HttpResponse getAPathArgument=__file__
140140
return resp # $ SPURIOUS: HttpResponse mimetype=application/json responseBody=resp
141141

142142

143143
@app.get("/file_response2", response_class=fastapi.responses.FileResponse) # $ routeSetup="/file_response2"
144144
async def file_response2(): # $ requestHandler
145-
return __file__ # $ HttpResponse
145+
return __file__ # $ HttpResponse getAPathArgument=__file__

python/ql/test/library-tests/frameworks/fastapi/router.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# like blueprints in Flask
22
# see https://fastapi.tiangolo.com/tutorial/bigger-applications/
3+
# see basic.py for instructions for how to run this code.
34

45
from fastapi import APIRouter, FastAPI
56

@@ -30,4 +31,23 @@ async def items(): # $ requestHandler
3031
app.include_router(outer_router, prefix="/outer")
3132
app.include_router(items_router)
3233

33-
# see basic.py for instructions for how to run this code.
34+
# Using a custom router
35+
36+
class MyCustomRouter(APIRouter):
37+
"""
38+
Which automatically removes trailing slashes
39+
"""
40+
def api_route(self, path: str, **kwargs):
41+
path = path.rstrip("/")
42+
return super().api_route(path, **kwargs)
43+
44+
45+
custom_router = MyCustomRouter()
46+
47+
48+
@custom_router.get("/bar/") # $ routeSetup="/bar/"
49+
async def items(): # $ requestHandler
50+
return {"msg": "custom_router /bar/"} # $ HttpResponse
51+
52+
53+
app.include_router(custom_router)

0 commit comments

Comments
 (0)