Skip to content

Commit c839f35

Browse files
committed
Python: FastAPI: Proper modeling of implicit returns
1 parent 5014770 commit c839f35

2 files changed

Lines changed: 68 additions & 25 deletions

File tree

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

Lines changed: 62 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ private module FastApi {
7373
override Function getARequestHandler() { result.getADecorator().getAFlowNode() = node }
7474

7575
override string getFramework() { result = "FastAPI" }
76+
77+
/** Gets the argument specifying the response class to use, if any. */
78+
DataFlow::Node getResponseClassArg() { result = this.getArgByName("response_class") }
7679
}
7780

7881
// ---------------------------------------------------------------------------
@@ -199,6 +202,65 @@ private module FastApi {
199202
}
200203
}
201204

205+
/**
206+
* An implicit response from a return of FastAPI request handler.
207+
*/
208+
private class FastApiRequestHandlerReturn extends HTTP::Server::HttpResponse::Range,
209+
DataFlow::CfgNode {
210+
FastApiRouteSetup routeSetup;
211+
212+
FastApiRequestHandlerReturn() {
213+
node = routeSetup.getARequestHandler().getAReturnValueFlowNode()
214+
}
215+
216+
override DataFlow::Node getBody() { result = this }
217+
218+
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
219+
220+
override string getMimetypeDefault() {
221+
exists(API::Node responseClass |
222+
responseClass.getAUse() = routeSetup.getResponseClassArg() and
223+
result = getDefaultMimeType(responseClass)
224+
)
225+
or
226+
not exists(routeSetup.getResponseClassArg()) and
227+
result = "application/json"
228+
}
229+
}
230+
231+
/**
232+
* An implicit response from a return of FastAPI request handler, that has
233+
* `response_class` set to a `FileResponse`.
234+
*/
235+
private class FastApiRequestHandlerFileResponseReturn extends FastApiRequestHandlerReturn {
236+
FastApiRequestHandlerFileResponseReturn() {
237+
exists(API::Node responseClass |
238+
responseClass.getAUse() = routeSetup.getResponseClassArg() and
239+
responseClass = getModeledResponseClass("FileResponse").getASubclass*()
240+
)
241+
}
242+
243+
override DataFlow::Node getBody() { none() }
244+
}
245+
246+
/**
247+
* An implicit response from a return of FastAPI request handler, that has
248+
* `response_class` set to a `RedirectResponse`.
249+
*/
250+
private class FastApiRequestHandlerRedirectReturn extends FastApiRequestHandlerReturn,
251+
HTTP::Server::HttpRedirectResponse::Range {
252+
FastApiRequestHandlerRedirectReturn() {
253+
exists(API::Node responseClass |
254+
responseClass.getAUse() = routeSetup.getResponseClassArg() and
255+
responseClass = getModeledResponseClass("RedirectResponse").getASubclass*()
256+
)
257+
}
258+
259+
override DataFlow::Node getBody() { none() }
260+
261+
override DataFlow::Node getRedirectLocation() { result = this }
262+
}
263+
202264
/**
203265
* INTERNAL: Do not use.
204266
*
@@ -263,23 +325,4 @@ private module FastApi {
263325
override DataFlow::Node getValueArg() { none() }
264326
}
265327
}
266-
267-
/**
268-
* Implicit response from returns of FastAPI request handlers
269-
*/
270-
private class FastApiRequestHandlerReturn extends HTTP::Server::HttpResponse::Range,
271-
DataFlow::CfgNode {
272-
FastApiRequestHandlerReturn() {
273-
exists(Function requestHandler |
274-
requestHandler = any(FastApiRouteSetup rs).getARequestHandler() and
275-
node = requestHandler.getAReturnValueFlowNode()
276-
)
277-
}
278-
279-
override DataFlow::Node getBody() { result = this }
280-
281-
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
282-
283-
override string getMimetypeDefault() { result = "application/json" }
284-
}
285328
}

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ async def response_parameter_custom_type(response: MyXmlResponse): # $ requestHa
4747
response.set_cookie("key", "value") # $ CookieWrite CookieName="key" CookieValue="value"
4848
response.headers["Custom-Response-Type"] = "yes, but only after function has run"
4949
xml_data = "<foo>FOO</foo>"
50-
return xml_data # $ HttpResponse responseBody=xml_data SPURIOUS: mimetype=application/json MISSING: mimetype=application/xml
50+
return xml_data # $ HttpResponse responseBody=xml_data mimetype=application/xml
5151

5252

5353
# Direct response construction
@@ -68,7 +68,7 @@ async def direct_response(): # $ requestHandler
6868
@app.get("/direct_response2", response_class=fastapi.responses.Response) # $ routeSetup="/direct_response2"
6969
async def direct_response2(): # $ requestHandler
7070
xml_data = "<foo>FOO</foo>"
71-
return xml_data # $ HttpResponse responseBody=xml_data SPURIOUS: mimetype=application/json
71+
return xml_data # $ HttpResponse responseBody=xml_data
7272

7373

7474
@app.get("/my_xml_response") # $ routeSetup="/my_xml_response"
@@ -81,7 +81,7 @@ async def my_xml_response(): # $ requestHandler
8181
@app.get("/my_xml_response2", response_class=MyXmlResponse) # $ routeSetup="/my_xml_response2"
8282
async def my_xml_response2(): # $ requestHandler
8383
xml_data = "<foo>FOO</foo>"
84-
return xml_data # $ HttpResponse responseBody=xml_data SPURIOUS: mimetype=application/json MISSING: mimetype=application/xml
84+
return xml_data # $ HttpResponse responseBody=xml_data mimetype=application/xml
8585

8686

8787
@app.get("/html_response") # $ routeSetup="/html_response"
@@ -94,7 +94,7 @@ async def html_response(): # $ requestHandler
9494
@app.get("/html_response2", response_class=fastapi.responses.HTMLResponse) # $ routeSetup="/html_response2"
9595
async def html_response2(): # $ requestHandler
9696
hello_world = "<h1>Hello World!</h1>"
97-
return hello_world # $ HttpResponse responseBody=hello_world SPURIOUS: mimetype=application/json MISSING: mimetype=text/html
97+
return hello_world # $ HttpResponse responseBody=hello_world mimetype=text/html
9898

9999

100100
@app.get("/redirect") # $ routeSetup="/redirect"
@@ -107,7 +107,7 @@ async def redirect(): # $ requestHandler
107107
@app.get("/redirect2", response_class=fastapi.responses.RedirectResponse) # $ routeSetup="/redirect2"
108108
async def redirect2(): # $ requestHandler
109109
next = "https://www.example.com"
110-
return next # $ HttpResponse SPURIOUS: mimetype=application/json responseBody=next MISSING: HttpRedirectResponse redirectLocation=next
110+
return next # $ HttpResponse HttpRedirectResponse redirectLocation=next
111111

112112

113113
@app.get("/streaming_response") # $ routeSetup="/streaming_response"
@@ -142,4 +142,4 @@ async def file_response(): # $ requestHandler
142142

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

0 commit comments

Comments
 (0)