Skip to content

Commit 5d4140d

Browse files
committed
Python: Handle more complicated route-setup in aiohttp
Since we want to be able to easy select request-handlers that are not set up as part of a view-class, we need to easily be able to identify those. To handle cases like the one below, we _can't_ just define these to be all the async functions that are not methods on a class :( ```py # see https://docs.aiohttp.org/en/stable/web_quickstart.html#organizing-handlers-in-classes class MyCustomHandlerClass: async def foo_handler(self, request): # $ MISSING: requestHandler return web.Response(text="MyCustomHandlerClass.foo") my_custom_handler = MyCustomHandlerClass() app.router.add_get("/MyCustomHandlerClass/foo", my_custom_handler.foo_handler) # $ routeSetup="/MyCustomHandlerClass/foo" ``` So it seemed easiest to narrow down the route-setups, but that means we want both refinement and extensibility... so `::Range` pattern to the rescue 🎉 The important piece of code that still works after this commit, but which hasn't been changed, is the one below: ```codeql /** * A parameter that will receive a `aiohttp.web.Request` instance when a request * handler is invoked. */ class AiohttpRequestHandlerRequestParam extends Request::InstanceSource, RemoteFlowSource::Range, DataFlow::ParameterNode { AiohttpRequestHandlerRequestParam() { exists(Function requestHandler | requestHandler = any(AiohttpCoroutineRouteSetup setup).getARequestHandler() and ```
1 parent 919a0b6 commit 5d4140d

File tree

2 files changed

+111
-130
lines changed

2 files changed

+111
-130
lines changed

python/ql/src/semmle/python/frameworks/Aiohttp.qll

Lines changed: 110 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -50,110 +50,130 @@ module AiohttpWebModel {
5050
result = applicationInstance().getMember("router")
5151
}
5252

53-
/** A route setup in `aiohttp.web` */
54-
abstract class AiohttpRouteSetup extends HTTP::Server::RouteSetup::Range {
53+
/**
54+
* A route setup in `aiohttp.web`. Since all route-setups can technically use either
55+
* coroutines or view-classes as the handler argument (although that's not how you're
56+
* **supposed** to do things), we also need to handle this.
57+
*
58+
* Extend this class to refine existing API models. If you want to model new APIs,
59+
* extend `AiohttpRouteSetup::Range` instead.
60+
*/
61+
class AiohttpRouteSetup extends HTTP::Server::RouteSetup::Range {
62+
AiohttpRouteSetup::Range range;
63+
64+
AiohttpRouteSetup() { this = range }
65+
5566
override Parameter getARoutedParameter() { none() }
5667

5768
override string getFramework() { result = "aiohttp.web" }
58-
}
59-
60-
/** An aiohttp route setup that uses coroutines (async function) as request handler. */
61-
abstract class AiohttpCoroutineRouteSetup extends AiohttpRouteSetup {
62-
/** Gets the argument specifying the request handler (which is a coroutine/async function) */
63-
abstract DataFlow::Node getRequestHandlerArg();
6469

65-
override Function getARequestHandler() {
66-
this.getRequestHandlerArg() = poorMansFunctionTracker(result)
67-
}
68-
}
70+
/** Gets the argument specifying the handler (either a coroutine or a view-class). */
71+
DataFlow::Node getHandlerArg() { result = range.getHandlerArg() }
6972

70-
/**
71-
* Gets a reference to a class, that has been backtracked from the view-class handler
72-
* argument `origin` (to a route-setup for view-classes).
73-
*/
74-
private DataFlow::LocalSourceNode viewClassBackTracker(
75-
DataFlow::TypeBackTracker t, DataFlow::Node origin
76-
) {
77-
t.start() and
78-
origin = any(AiohttpViewRouteSetup rs).getViewClassArg() and
79-
result = origin.getALocalSource()
80-
or
81-
exists(DataFlow::TypeBackTracker t2 |
82-
result = viewClassBackTracker(t2, origin).backtrack(t2, t)
83-
)
84-
}
73+
override DataFlow::Node getUrlPatternArg() { result = range.getUrlPatternArg() }
8574

86-
/**
87-
* Gets a reference to a class, that has been backtracked from the view-class handler
88-
* argument `origin` (to a route-setup for view-classes).
89-
*/
90-
DataFlow::LocalSourceNode viewClassBackTracker(DataFlow::Node origin) {
91-
result = viewClassBackTracker(DataFlow::TypeBackTracker::end(), origin)
92-
}
75+
/** Gets the view-class that is referenced in the view-class handler argument, if any. */
76+
Class getViewClass() { result = range.getViewClass() }
9377

94-
Class getBackTrackedViewClass(DataFlow::Node origin) {
95-
result.getParent() = viewClassBackTracker(origin).asExpr()
78+
override Function getARequestHandler() { result = range.getARequestHandler() }
9679
}
9780

98-
/** An aiohttp route setup that uses view-classes as request handlers. */
99-
abstract class AiohttpViewRouteSetup extends AiohttpRouteSetup {
100-
/** Gets the argument specifying the view-class handler. */
101-
abstract DataFlow::Node getViewClassArg();
102-
103-
/** Gets the view-class that is referenced in the view-class handler argument. */
104-
Class getViewClass() { result = getBackTrackedViewClass(this.getViewClassArg()) }
105-
106-
override Function getARequestHandler() {
107-
exists(AiohttpViewClass cls |
108-
cls = this.getViewClass() and
109-
result = cls.getARequestHandler()
110-
)
81+
/** Provides a class for modeling new aiohttp.web route setups. */
82+
private module AiohttpRouteSetup {
83+
/**
84+
* A route setup in `aiohttp.web`. Since all route-setups can technically use either
85+
* coroutines or view-classes as the handler argument (although that's not how you're
86+
* **supposed** to do things), we also need to handle this.
87+
*
88+
* Extend this class to model new APIs. If you want to refine existing API models,
89+
* extend `AiohttpRouteSetup` instead.
90+
*/
91+
abstract class Range extends DataFlow::Node {
92+
/** Gets the argument used to set the URL pattern. */
93+
abstract DataFlow::Node getUrlPatternArg();
94+
95+
/** Gets the argument specifying the handler (either a coroutine or a view-class). */
96+
abstract DataFlow::Node getHandlerArg();
97+
98+
/** Gets the view-class that is referenced in the view-class handler argument, if any. */
99+
Class getViewClass() { result = getBackTrackedViewClass(this.getHandlerArg()) }
100+
101+
/**
102+
* Gets a function that will handle incoming requests for this route, if any.
103+
*
104+
* NOTE: This will be modified in the near future to have a `RequestHandler` result, instead of a `Function`.
105+
*/
106+
Function getARequestHandler() {
107+
this.getHandlerArg() = poorMansFunctionTracker(result)
108+
or
109+
exists(AiohttpViewClass cls |
110+
cls = this.getViewClass() and
111+
result = cls.getARequestHandler()
112+
)
113+
}
111114
}
112-
}
113115

114-
/**
115-
* A route-setup from `add_route` or any of `add_get`, `add_post`, etc. on an
116-
* `aiohttp.web.UrlDispatcher`.
117-
*/
118-
class AiohttpUrlDispatcherAddRouteCall extends AiohttpCoroutineRouteSetup, DataFlow::CallCfgNode {
119-
/** At what index route arguments starts, so we can handle "route" version together with get/post/... */
120-
int routeArgsStart;
121-
122-
AiohttpUrlDispatcherAddRouteCall() {
123-
this = urlDispathcerInstance().getMember("add_" + HTTP::httpVerbLower()).getACall() and
124-
routeArgsStart = 0
116+
/**
117+
* Gets a reference to a class, that has been backtracked from the view-class handler
118+
* argument `origin` (to a route-setup for view-classes).
119+
*/
120+
private DataFlow::LocalSourceNode viewClassBackTracker(
121+
DataFlow::TypeBackTracker t, DataFlow::Node origin
122+
) {
123+
t.start() and
124+
origin = any(Range rs).getHandlerArg() and
125+
result = origin.getALocalSource()
125126
or
126-
this = urlDispathcerInstance().getMember("add_route").getACall() and
127-
routeArgsStart = 1
127+
exists(DataFlow::TypeBackTracker t2 |
128+
result = viewClassBackTracker(t2, origin).backtrack(t2, t)
129+
)
128130
}
129131

130-
override DataFlow::Node getUrlPatternArg() {
131-
result in [this.getArg(routeArgsStart + 0), this.getArgByName("path")]
132+
/**
133+
* Gets a reference to a class, that has been backtracked from the view-class handler
134+
* argument `origin` (to a route-setup for view-classes).
135+
*/
136+
DataFlow::LocalSourceNode viewClassBackTracker(DataFlow::Node origin) {
137+
result = viewClassBackTracker(DataFlow::TypeBackTracker::end(), origin)
132138
}
133139

134-
override DataFlow::Node getRequestHandlerArg() {
135-
result in [this.getArg(routeArgsStart + 1), this.getArgByName("handler")]
140+
Class getBackTrackedViewClass(DataFlow::Node origin) {
141+
result.getParent() = viewClassBackTracker(origin).asExpr()
136142
}
137143
}
138144

145+
/** An aiohttp route setup that uses coroutines (async function) as request handler. */
146+
class AiohttpCoroutineRouteSetup extends AiohttpRouteSetup {
147+
AiohttpCoroutineRouteSetup() { this.getHandlerArg() = poorMansFunctionTracker(_) }
148+
}
149+
150+
/** An aiohttp route setup that uses view-classes as request handlers. */
151+
class AiohttpViewRouteSetup extends AiohttpRouteSetup {
152+
AiohttpViewRouteSetup() { exists(this.getViewClass()) }
153+
}
154+
139155
/**
140-
* A route-setup from using `route` or any of `get`, `post`, etc. functions from `aiohttp.web`.
141-
*
142-
* Note that technically, this does not set up a route in itself (since it needs to be added to an application first).
143-
* However, modeling this way makes it easier, and we don't expect it to lead to many problems.
156+
* A route-setup from
157+
* - `add_route`, `add_view`, `add_get`, `add_post`, , etc. on an `aiohttp.web.UrlDispatcher`.
158+
* - `route`, `view`, `get`, `post`, etc. functions from `aiohttp.web`.
144159
*/
145-
class AiohttpWebRouteCall extends AiohttpCoroutineRouteSetup, DataFlow::CallCfgNode {
160+
class AiohttpAddRouteCall extends AiohttpRouteSetup::Range, DataFlow::CallCfgNode {
146161
/** At what index route arguments starts, so we can handle "route" version together with get/post/... */
147162
int routeArgsStart;
148163

149-
AiohttpWebRouteCall() {
164+
AiohttpAddRouteCall() {
150165
exists(string funcName |
151166
funcName = HTTP::httpVerbLower() and
152167
routeArgsStart = 0
153168
or
169+
funcName = "view" and
170+
routeArgsStart = 0
171+
or
154172
funcName = "route" and
155173
routeArgsStart = 1
156174
|
175+
this = urlDispathcerInstance().getMember("add_" + funcName).getACall()
176+
or
157177
this = API::moduleImport("aiohttp").getMember("web").getMember(funcName).getACall()
158178
)
159179
}
@@ -162,21 +182,24 @@ module AiohttpWebModel {
162182
result in [this.getArg(routeArgsStart + 0), this.getArgByName("path")]
163183
}
164184

165-
override DataFlow::Node getRequestHandlerArg() {
185+
override DataFlow::Node getHandlerArg() {
166186
result in [this.getArg(routeArgsStart + 1), this.getArgByName("handler")]
167187
}
168188
}
169189

170-
/** A route-setup from using a `route` or any of `get`, `post`, etc. decorators from a `aiohttp.web.RouteTableDef`. */
171-
class AiohttpRouteTableDefRouteCall extends AiohttpCoroutineRouteSetup, DataFlow::CallCfgNode {
190+
/** A route-setup using a decorator, such as `route`, `view`, `get`, `post`, etc. on a `aiohttp.web.RouteTableDef`. */
191+
class AiohttpDecoratorRouteSetup extends AiohttpRouteSetup::Range, DataFlow::CallCfgNode {
172192
/** At what index route arguments starts, so we can handle "route" version together with get/post/... */
173193
int routeArgsStart;
174194

175-
AiohttpRouteTableDefRouteCall() {
195+
AiohttpDecoratorRouteSetup() {
176196
exists(string decoratorName |
177197
decoratorName = HTTP::httpVerbLower() and
178198
routeArgsStart = 0
179199
or
200+
decoratorName = "view" and
201+
routeArgsStart = 0
202+
or
180203
decoratorName = "route" and
181204
routeArgsStart = 1
182205
|
@@ -194,63 +217,21 @@ module AiohttpWebModel {
194217
result in [this.getArg(routeArgsStart + 0), this.getArgByName("path")]
195218
}
196219

197-
override DataFlow::Node getRequestHandlerArg() { none() }
220+
override DataFlow::Node getHandlerArg() { none() }
198221

199-
override Function getARequestHandler() { result.getADecorator() = this.asExpr() }
200-
}
222+
override Class getViewClass() { result.getADecorator() = this.asExpr() }
201223

202-
/**
203-
* A view-class route-setup from either:
204-
* - `add_view` method on a `aiohttp.web.UrlDispatcher`
205-
* - `view` function from `aiohttp.web`
206-
*/
207-
class AiohttpViewRouteSetupFromFunction extends AiohttpViewRouteSetup, DataFlow::CallCfgNode {
208-
AiohttpViewRouteSetupFromFunction() {
209-
this = urlDispathcerInstance().getMember("add_view").getACall()
210-
or
211-
this = API::moduleImport("aiohttp").getMember("web").getMember("view").getACall()
224+
override Function getARequestHandler() {
225+
// we're decorating a class
226+
exists(this.getViewClass()) and
227+
result = super.getARequestHandler()
212228
or
213-
this =
214-
API::moduleImport("aiohttp")
215-
.getMember("web")
216-
.getMember("RouteTableDef")
217-
.getReturn()
218-
.getMember("view")
219-
.getACall()
220-
}
221-
222-
override DataFlow::Node getUrlPatternArg() {
223-
result in [this.getArg(0), this.getArgByName("path")]
224-
}
225-
226-
override DataFlow::Node getViewClassArg() {
227-
result in [this.getArg(1), this.getArgByName("handler")]
229+
// we're decorating a function
230+
not exists(this.getViewClass()) and
231+
result.getADecorator() = this.asExpr()
228232
}
229233
}
230234

231-
/**
232-
* A view-class route-setup from the `view` decorator from a `aiohttp.web.RouteTableDef`.
233-
*/
234-
class AiohttpViewRouteSetupFromDecorator extends AiohttpViewRouteSetup, DataFlow::CallCfgNode {
235-
AiohttpViewRouteSetupFromDecorator() {
236-
this =
237-
API::moduleImport("aiohttp")
238-
.getMember("web")
239-
.getMember("RouteTableDef")
240-
.getReturn()
241-
.getMember("view")
242-
.getACall()
243-
}
244-
245-
override DataFlow::Node getUrlPatternArg() {
246-
result in [this.getArg(0), this.getArgByName("path")]
247-
}
248-
249-
override DataFlow::Node getViewClassArg() { none() }
250-
251-
override Class getViewClass() { result.getADecorator() = this.asExpr() }
252-
}
253-
254235
/** A class that we consider a aiohttp.web View class. */
255236
abstract class AiohttpViewClass extends Class, SelfRefMixin {
256237
/** Gets a function that could handle incoming requests, if any. */
@@ -269,7 +250,7 @@ module AiohttpWebModel {
269250

270251
/** A class that is used in a route-setup, therefore being considered a aiohttp.web View class. */
271252
class AiohttpViewClassFromRouteSetup extends AiohttpViewClass {
272-
AiohttpViewClassFromRouteSetup() { this = any(AiohttpViewRouteSetup rs).getViewClass() }
253+
AiohttpViewClassFromRouteSetup() { this = any(AiohttpRouteSetup rs).getViewClass() }
273254
}
274255

275256
/** A request handler defined in an `aiohttp.web` view class, that has no known route. */

python/ql/test/library-tests/frameworks/aiohttp/routing_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ async def get(self): # $ requestHandler
126126
# Apparently there is no enforcement that `add_view` is only for views, and vice-versa
127127
# for `add_get` only being for async functions.
128128
if True:
129-
async def no_rules(request): # $ MISSING: requestHandler
129+
async def no_rules(request): # $ requestHandler
130130
return web.Response(text="no_rules")
131131

132132
app.router.add_view("/no_rules", no_rules) # $ routeSetup="/no_rules"

0 commit comments

Comments
 (0)