Skip to content

Commit 22b35f5

Browse files
authored
Merge pull request #18318 from RasmusWL/fastapi-request
Python: Model FastAPI requests
2 parents 30dbc3b + a9704d8 commit 22b35f5

File tree

3 files changed

+96
-0
lines changed

3 files changed

+96
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Added modeling of `fastapi.Request` and `starlette.requests.Request` as sources of untrusted input,
5+
and modeling of tainted data flow out of these request objects.

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

+56
Original file line numberDiff line numberDiff line change
@@ -245,4 +245,60 @@ module Starlette {
245245

246246
override DataFlow::Node getAPathArgument() { result = this.getParameter(0, "path").asSink() }
247247
}
248+
249+
/**
250+
* Provides models for the `starlette.requests.Request` class
251+
*
252+
* See https://www.starlette.io/requests/.
253+
*/
254+
module Request {
255+
/** Gets a reference to the `starlette.requests.Request` class. */
256+
API::Node classRef() {
257+
result = API::moduleImport("starlette").getMember("requests").getMember("Request")
258+
or
259+
result = API::moduleImport("fastapi").getMember("Request")
260+
}
261+
262+
/**
263+
* A source of instances of `starlette.requests.Request`, extend this class to model new instances.
264+
*
265+
* This can include instantiations of the class, return values from function
266+
* calls, or a special parameter that will be set when functions are called by an external
267+
* library.
268+
*
269+
* Use the predicate `Request::instance()` to get references to instances of `starlette.requests.Request`.
270+
*/
271+
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
272+
273+
/** A direct instantiation of `starlette.requests.Request`. */
274+
private class ClassInstantiation extends InstanceSource {
275+
ClassInstantiation() { this = classRef().getAnInstance().asSource() }
276+
}
277+
278+
/** Gets a reference to an instance of `starlette.requests.Request`. */
279+
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
280+
t.start() and
281+
result instanceof InstanceSource
282+
or
283+
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
284+
}
285+
286+
/** Gets a reference to an instance of `starlette.requests.Request`. */
287+
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
288+
289+
/**
290+
* Taint propagation for `starlette.requests.Request`.
291+
*/
292+
private class InstanceTaintSteps extends InstanceTaintStepsHelper {
293+
InstanceTaintSteps() { this = "starlette.requests.Request" }
294+
295+
override DataFlow::Node getInstance() { result = instance() }
296+
297+
override string getAttributeName() { result in ["cookies"] }
298+
299+
override string getMethodName() { none() }
300+
301+
override string getAsyncMethodName() { result in ["body", "json", "form", "stream"] }
302+
}
303+
}
248304
}

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

+35
Original file line numberDiff line numberDiff line change
@@ -187,3 +187,38 @@ async def websocket_test(websocket: WebSocket): # $ requestHandler routedParamet
187187

188188
async for data in websocket.iter_json():
189189
ensure_tainted(data) # $ tainted
190+
191+
192+
# --- Request ---
193+
194+
import starlette.requests
195+
from fastapi import Request
196+
197+
198+
assert Request == starlette.requests.Request
199+
200+
@app.websocket("/req") # $ routeSetup="/req"
201+
async def request_test(request: Request): # $ requestHandler routedParameter=request
202+
ensure_tainted(
203+
request, # $ tainted
204+
205+
await request.body(), # $ tainted
206+
207+
await request.json(), # $ tainted
208+
await request.json()["key"], # $ tainted
209+
210+
# form() returns a FormData (which is a starlette ImmutableMultiDict)
211+
await request.form(), # $ tainted
212+
await request.form()["key"], # $ tainted
213+
await request.form().getlist("key"), # $ MISSING: tainted
214+
await request.form().getlist("key")[0], # $ MISSING: tainted
215+
# data in the form could be an starlette.datastructures.UploadFile
216+
await request.form()["file"].filename, # $ MISSING: tainted
217+
await request.form().getlist("file")[0].filename, # $ MISSING: tainted
218+
219+
request.cookies, # $ tainted
220+
request.cookies["key"], # $ tainted
221+
)
222+
223+
async for chunk in request.stream():
224+
ensure_tainted(chunk) # $ tainted

0 commit comments

Comments
 (0)