Skip to content

Commit 0240631

Browse files
authored
Merge pull request #6782 from RasmusWL/fastapi
Python: Model FastAPI
2 parents 54fba2d + c52e453 commit 0240631

File tree

15 files changed

+1170
-0
lines changed

15 files changed

+1170
-0
lines changed

docs/codeql/support/reusables/frameworks.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ Python built-in support
155155
Name, Category
156156
aiohttp.web, Web framework
157157
Django, Web framework
158+
FastAPI, Web framework
158159
Flask, Web framework
159160
Tornado, Web framework
160161
Twisted, Web framework
@@ -169,6 +170,7 @@ Python built-in support
169170
invoke, Utility library
170171
jmespath, Utility library
171172
multidict, Utility library
173+
pydantic, Utility library
172174
yarl, Utility library
173175
aioch, Database
174176
asyncpg, Database
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lgtm,codescanning
2+
* Added modeling of sources/sinks when using FastAPI to create web servers.

python/ql/lib/semmle/python/Frameworks.qll

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ private import semmle.python.frameworks.Cryptography
1313
private import semmle.python.frameworks.Dill
1414
private import semmle.python.frameworks.Django
1515
private import semmle.python.frameworks.Fabric
16+
private import semmle.python.frameworks.FastApi
1617
private import semmle.python.frameworks.Flask
1718
private import semmle.python.frameworks.FlaskSqlAlchemy
1819
private import semmle.python.frameworks.Idna
@@ -24,11 +25,13 @@ private import semmle.python.frameworks.Mysql
2425
private import semmle.python.frameworks.MySQLdb
2526
private import semmle.python.frameworks.Peewee
2627
private import semmle.python.frameworks.Psycopg2
28+
private import semmle.python.frameworks.Pydantic
2729
private import semmle.python.frameworks.PyMySQL
2830
private import semmle.python.frameworks.Rsa
2931
private import semmle.python.frameworks.RuamelYaml
3032
private import semmle.python.frameworks.Simplejson
3133
private import semmle.python.frameworks.SqlAlchemy
34+
private import semmle.python.frameworks.Starlette
3235
private import semmle.python.frameworks.Stdlib
3336
private import semmle.python.frameworks.Tornado
3437
private import semmle.python.frameworks.Twisted
Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
/**
2+
* Provides classes modeling security-relevant aspects of the `fastapi` PyPI package.
3+
* See https://fastapi.tiangolo.com/.
4+
*/
5+
6+
private import python
7+
private import semmle.python.dataflow.new.DataFlow
8+
private import semmle.python.dataflow.new.RemoteFlowSources
9+
private import semmle.python.dataflow.new.TaintTracking
10+
private import semmle.python.Concepts
11+
private import semmle.python.ApiGraphs
12+
private import semmle.python.frameworks.Pydantic
13+
private import semmle.python.frameworks.Starlette
14+
15+
/**
16+
* Provides models for the `fastapi` PyPI package.
17+
* See https://fastapi.tiangolo.com/.
18+
*/
19+
private module FastApi {
20+
/**
21+
* Provides models for FastAPI applications (an instance of `fastapi.FastAPI`).
22+
*/
23+
module App {
24+
/** Gets a reference to a FastAPI application (an instance of `fastapi.FastAPI`). */
25+
API::Node instance() { result = API::moduleImport("fastapi").getMember("FastAPI").getReturn() }
26+
}
27+
28+
/**
29+
* Provides models for the `fastapi.APIRouter` class
30+
*
31+
* See https://fastapi.tiangolo.com/tutorial/bigger-applications/.
32+
*/
33+
module APIRouter {
34+
/** Gets a reference to an instance of `fastapi.APIRouter`. */
35+
API::Node instance() {
36+
result = API::moduleImport("fastapi").getMember("APIRouter").getReturn()
37+
}
38+
}
39+
40+
// ---------------------------------------------------------------------------
41+
// routing modeling
42+
// ---------------------------------------------------------------------------
43+
/**
44+
* A call to a method like `get` or `post` on a FastAPI application.
45+
*
46+
* See https://fastapi.tiangolo.com/tutorial/first-steps/#define-a-path-operation-decorator
47+
*/
48+
private class FastApiRouteSetup extends HTTP::Server::RouteSetup::Range, DataFlow::CallCfgNode {
49+
FastApiRouteSetup() {
50+
exists(string routeAddingMethod |
51+
routeAddingMethod = HTTP::httpVerbLower()
52+
or
53+
routeAddingMethod in ["api_route", "websocket"]
54+
|
55+
this = App::instance().getMember(routeAddingMethod).getACall()
56+
or
57+
this = APIRouter::instance().getMember(routeAddingMethod).getACall()
58+
)
59+
}
60+
61+
override Parameter getARoutedParameter() {
62+
// this will need to be refined a bit, since you can add special parameters to
63+
// your request handler functions that are used to pass in the response. There
64+
// might be other special cases as well, but as a start this is not too far off
65+
// the mark.
66+
result = this.getARequestHandler().getArgByName(_) and
67+
// type-annotated with `Response`
68+
not any(Response::RequestHandlerParam src).asExpr() = result
69+
}
70+
71+
override DataFlow::Node getUrlPatternArg() {
72+
result in [this.getArg(0), this.getArgByName("path")]
73+
}
74+
75+
override Function getARequestHandler() { result.getADecorator().getAFlowNode() = node }
76+
77+
override string getFramework() { result = "FastAPI" }
78+
79+
/** Gets the argument specifying the response class to use, if any. */
80+
DataFlow::Node getResponseClassArg() { result = this.getArgByName("response_class") }
81+
}
82+
83+
/**
84+
* A parameter to a request handler that has a type-annotation with a class that is a
85+
* Pydantic model.
86+
*/
87+
private class PydanticModelRequestHandlerParam extends Pydantic::BaseModel::InstanceSource,
88+
DataFlow::ParameterNode {
89+
PydanticModelRequestHandlerParam() {
90+
this.getParameter().getAnnotation() = Pydantic::BaseModel::subclassRef().getAUse().asExpr() and
91+
any(FastApiRouteSetup rs).getARequestHandler().getArgByName(_) = this.getParameter()
92+
}
93+
}
94+
95+
// ---------------------------------------------------------------------------
96+
// Response modeling
97+
// ---------------------------------------------------------------------------
98+
/**
99+
* A parameter to a request handler that has a WebSocket type-annotation.
100+
*/
101+
private class WebSocketRequestHandlerParam extends Starlette::WebSocket::InstanceSource,
102+
DataFlow::ParameterNode {
103+
WebSocketRequestHandlerParam() {
104+
this.getParameter().getAnnotation() = Starlette::WebSocket::classRef().getAUse().asExpr() and
105+
any(FastApiRouteSetup rs).getARequestHandler().getArgByName(_) = this.getParameter()
106+
}
107+
}
108+
109+
/**
110+
* Provides models for the `fastapi.Response` class and subclasses.
111+
*
112+
* See https://fastapi.tiangolo.com/advanced/custom-response/#response.
113+
*/
114+
module Response {
115+
/**
116+
* Gets the `API::Node` for the manually modeled response classes called `name`.
117+
*/
118+
private API::Node getModeledResponseClass(string name) {
119+
name = "Response" and
120+
result = API::moduleImport("fastapi").getMember(name)
121+
or
122+
// see https://github.com/tiangolo/fastapi/blob/master/fastapi/responses.py
123+
name in [
124+
"Response", "HTMLResponse", "PlainTextResponse", "JSONResponse", "UJSONResponse",
125+
"ORJSONResponse", "RedirectResponse", "StreamingResponse", "FileResponse"
126+
] and
127+
result = API::moduleImport("fastapi").getMember("responses").getMember(name)
128+
}
129+
130+
/**
131+
* Gets the default MIME type for a FastAPI response class (defined with the
132+
* `media_type` class-attribute).
133+
*
134+
* Also models user-defined classes and tries to take inheritance into account.
135+
*
136+
* TODO: build easy way to solve problems like this, like we used to have the
137+
* `ClassValue.lookup` predicate.
138+
*/
139+
private string getDefaultMimeType(API::Node responseClass) {
140+
exists(string name | responseClass = getModeledResponseClass(name) |
141+
// no defaults for these.
142+
name in ["Response", "RedirectResponse", "StreamingResponse"] and
143+
none()
144+
or
145+
// For `FileResponse` the code will guess what mimetype
146+
// to use, or fall back to "text/plain", but claiming that all responses will
147+
// have "text/plain" per default is also highly inaccurate, so just going to not
148+
// do anything about this.
149+
name = "FileResponse" and
150+
none()
151+
or
152+
name = "HTMLResponse" and
153+
result = "text/html"
154+
or
155+
name = "PlainTextResponse" and
156+
result = "text/plain"
157+
or
158+
name in ["JSONResponse", "UJSONResponse", "ORJSONResponse"] and
159+
result = "application/json"
160+
)
161+
or
162+
// user-defined subclasses
163+
exists(Class cls, API::Node base |
164+
base = getModeledResponseClass(_).getASubclass*() and
165+
cls.getABase() = base.getAUse().asExpr() and
166+
responseClass.getAnImmediateUse().asExpr().(ClassExpr) = cls.getParent()
167+
|
168+
exists(Assign assign | assign = cls.getAStmt() |
169+
assign.getATarget().(Name).getId() = "media_type" and
170+
result = assign.getValue().(StrConst).getText()
171+
)
172+
or
173+
// TODO: this should use a proper MRO calculation instead
174+
not exists(Assign assign | assign = cls.getAStmt() |
175+
assign.getATarget().(Name).getId() = "media_type"
176+
) and
177+
result = getDefaultMimeType(base)
178+
)
179+
}
180+
181+
/**
182+
* A source of instances of `fastapi.Response` and its' subclasses, extend this class to model new instances.
183+
*
184+
* This can include instantiations of the class, return values from function
185+
* calls, or a special parameter that will be set when functions are called by an external
186+
* library.
187+
*
188+
* Use the predicate `Response::instance()` to get references to instances of `fastapi.Response`.
189+
*/
190+
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
191+
192+
/** A direct instantiation of a response class. */
193+
private class ResponseInstantiation extends InstanceSource, HTTP::Server::HttpResponse::Range,
194+
DataFlow::CallCfgNode {
195+
API::Node baseApiNode;
196+
API::Node responseClass;
197+
198+
ResponseInstantiation() {
199+
baseApiNode = getModeledResponseClass(_) and
200+
responseClass = baseApiNode.getASubclass*() and
201+
this = responseClass.getACall()
202+
}
203+
204+
override DataFlow::Node getBody() {
205+
not baseApiNode = getModeledResponseClass(["RedirectResponse", "FileResponse"]) and
206+
result in [this.getArg(0), this.getArgByName("content")]
207+
}
208+
209+
override DataFlow::Node getMimetypeOrContentTypeArg() {
210+
not baseApiNode = getModeledResponseClass("RedirectResponse") and
211+
result in [this.getArg(3), this.getArgByName("media_type")]
212+
}
213+
214+
override string getMimetypeDefault() { result = getDefaultMimeType(responseClass) }
215+
}
216+
217+
/**
218+
* A direct instantiation of a redirect response.
219+
*/
220+
private class RedirectResponseInstantiation extends ResponseInstantiation,
221+
HTTP::Server::HttpRedirectResponse::Range {
222+
RedirectResponseInstantiation() { baseApiNode = getModeledResponseClass("RedirectResponse") }
223+
224+
override DataFlow::Node getRedirectLocation() {
225+
result in [this.getArg(0), this.getArgByName("url")]
226+
}
227+
}
228+
229+
/**
230+
* An implicit response from a return of FastAPI request handler.
231+
*/
232+
private class FastApiRequestHandlerReturn extends HTTP::Server::HttpResponse::Range,
233+
DataFlow::CfgNode {
234+
FastApiRouteSetup routeSetup;
235+
236+
FastApiRequestHandlerReturn() {
237+
node = routeSetup.getARequestHandler().getAReturnValueFlowNode()
238+
}
239+
240+
override DataFlow::Node getBody() { result = this }
241+
242+
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
243+
244+
override string getMimetypeDefault() {
245+
exists(API::Node responseClass |
246+
responseClass.getAUse() = routeSetup.getResponseClassArg() and
247+
result = getDefaultMimeType(responseClass)
248+
)
249+
or
250+
not exists(routeSetup.getResponseClassArg()) and
251+
result = "application/json"
252+
}
253+
}
254+
255+
/**
256+
* An implicit response from a return of FastAPI request handler, that has
257+
* `response_class` set to a `FileResponse`.
258+
*/
259+
private class FastApiRequestHandlerFileResponseReturn extends FastApiRequestHandlerReturn {
260+
FastApiRequestHandlerFileResponseReturn() {
261+
exists(API::Node responseClass |
262+
responseClass.getAUse() = routeSetup.getResponseClassArg() and
263+
responseClass = getModeledResponseClass("FileResponse").getASubclass*()
264+
)
265+
}
266+
267+
override DataFlow::Node getBody() { none() }
268+
}
269+
270+
/**
271+
* An implicit response from a return of FastAPI request handler, that has
272+
* `response_class` set to a `RedirectResponse`.
273+
*/
274+
private class FastApiRequestHandlerRedirectReturn extends FastApiRequestHandlerReturn,
275+
HTTP::Server::HttpRedirectResponse::Range {
276+
FastApiRequestHandlerRedirectReturn() {
277+
exists(API::Node responseClass |
278+
responseClass.getAUse() = routeSetup.getResponseClassArg() and
279+
responseClass = getModeledResponseClass("RedirectResponse").getASubclass*()
280+
)
281+
}
282+
283+
override DataFlow::Node getBody() { none() }
284+
285+
override DataFlow::Node getRedirectLocation() { result = this }
286+
}
287+
288+
/**
289+
* INTERNAL: Do not use.
290+
*
291+
* A parameter to a FastAPI request-handler that has a `fastapi.Response`
292+
* type-annotation.
293+
*/
294+
class RequestHandlerParam extends InstanceSource, DataFlow::ParameterNode {
295+
RequestHandlerParam() {
296+
this.getParameter().getAnnotation() =
297+
getModeledResponseClass(_).getASubclass*().getAUse().asExpr() and
298+
any(FastApiRouteSetup rs).getARequestHandler().getArgByName(_) = this.getParameter()
299+
}
300+
}
301+
302+
/** Gets a reference to an instance of `fastapi.Response`. */
303+
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
304+
t.start() and
305+
result instanceof InstanceSource
306+
or
307+
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
308+
}
309+
310+
/** Gets a reference to an instance of `fastapi.Response`. */
311+
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
312+
313+
/**
314+
* A call to `set_cookie` on a FastAPI Response.
315+
*/
316+
private class SetCookieCall extends HTTP::Server::CookieWrite::Range, DataFlow::MethodCallNode {
317+
SetCookieCall() { this.calls(instance(), "set_cookie") }
318+
319+
override DataFlow::Node getHeaderArg() { none() }
320+
321+
override DataFlow::Node getNameArg() { result in [this.getArg(0), this.getArgByName("key")] }
322+
323+
override DataFlow::Node getValueArg() {
324+
result in [this.getArg(1), this.getArgByName("value")]
325+
}
326+
}
327+
328+
/**
329+
* A call to `append` on a `headers` of a FastAPI Response, with the `Set-Cookie`
330+
* header-key.
331+
*/
332+
private class HeadersAppendCookie extends HTTP::Server::CookieWrite::Range,
333+
DataFlow::MethodCallNode {
334+
HeadersAppendCookie() {
335+
exists(DataFlow::AttrRead headers, DataFlow::Node keyArg |
336+
headers.accesses(instance(), "headers") and
337+
this.calls(headers, "append") and
338+
keyArg in [this.getArg(0), this.getArgByName("key")] and
339+
keyArg.getALocalSource().asExpr().(StrConst).getText().toLowerCase() = "set-cookie"
340+
)
341+
}
342+
343+
override DataFlow::Node getHeaderArg() {
344+
result in [this.getArg(1), this.getArgByName("value")]
345+
}
346+
347+
override DataFlow::Node getNameArg() { none() }
348+
349+
override DataFlow::Node getValueArg() { none() }
350+
}
351+
}
352+
}

0 commit comments

Comments
 (0)