Skip to content

Commit c4b618d

Browse files
committed
Python: Model view-classes in aiohttp.web
No taint modeling of them yet though
1 parent 8c039d5 commit c4b618d

File tree

3 files changed

+158
-12
lines changed

3 files changed

+158
-12
lines changed

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

Lines changed: 148 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,19 @@ private import semmle.python.frameworks.Yarl
2020
* See https://docs.aiohttp.org/en/stable/web.html
2121
*/
2222
module AiohttpWebModel {
23+
/**
24+
* Provides models for the `aiohttp.web.View` class and subclasses.
25+
*
26+
* See https://docs.aiohttp.org/en/stable/web_reference.html#view.
27+
*/
28+
module View {
29+
/** Gets a reference to the `flask.views.View` class or any subclass. */
30+
API::Node subclassRef() {
31+
result = API::moduleImport("aiohttp").getMember("web").getMember("View").getASubclass*()
32+
}
33+
}
34+
35+
// -- route modeling --
2336
/** Gets a reference to a `aiohttp.web.Application` instance. */
2437
API::Node applicationInstance() {
2538
// Not sure whether you're allowed to add routes _after_ starting the app, for
@@ -36,7 +49,6 @@ module AiohttpWebModel {
3649
result = applicationInstance().getMember("router")
3750
}
3851

39-
// -- route modeling --
4052
/** A route setup in `aiohttp.web` */
4153
abstract class AiohttpRouteSetup extends HTTP::Server::RouteSetup::Range {
4254
override Parameter getARoutedParameter() { none() }
@@ -54,6 +66,50 @@ module AiohttpWebModel {
5466
}
5567
}
5668

69+
/**
70+
* Gets a reference to a class, that has been backtracked from the view-class handler
71+
* argument `origin` (to a route-setup for view-classes).
72+
*/
73+
private DataFlow::LocalSourceNode viewClassBackTracker(
74+
DataFlow::TypeBackTracker t, DataFlow::Node origin
75+
) {
76+
t.start() and
77+
origin = any(AiohttpViewRouteSetup rs).getViewClassArg() and
78+
result = origin.getALocalSource()
79+
or
80+
exists(DataFlow::TypeBackTracker t2 |
81+
result = viewClassBackTracker(t2, origin).backtrack(t2, t)
82+
)
83+
}
84+
85+
/**
86+
* Gets a reference to a class, that has been backtracked from the view-class handler
87+
* argument `origin` (to a route-setup for view-classes).
88+
*/
89+
DataFlow::LocalSourceNode viewClassBackTracker(DataFlow::Node origin) {
90+
result = viewClassBackTracker(DataFlow::TypeBackTracker::end(), origin)
91+
}
92+
93+
Class getBackTrackedViewClass(DataFlow::Node origin) {
94+
result.getParent() = viewClassBackTracker(origin).asExpr()
95+
}
96+
97+
/** An aiohttp route setup that uses view-classes as request handlers. */
98+
abstract class AiohttpViewRouteSetup extends AiohttpRouteSetup {
99+
/** Gets the argument specifying the view-class handler. */
100+
abstract DataFlow::Node getViewClassArg();
101+
102+
/** Gets the view-class that is referenced in the view-class handler argument. */
103+
Class getViewClass() { result = getBackTrackedViewClass(this.getViewClassArg()) }
104+
105+
override Function getARequestHandler() {
106+
exists(AiohttpViewClass cls |
107+
cls = this.getViewClass() and
108+
result = cls.getARequestHandler()
109+
)
110+
}
111+
}
112+
57113
/**
58114
* A route-setup from `add_route` or any of `add_get`, `add_post`, etc. on an
59115
* `aiohttp.web.UrlDispatcher`.
@@ -142,6 +198,91 @@ module AiohttpWebModel {
142198
override Function getARequestHandler() { result.getADecorator() = this.asExpr() }
143199
}
144200

201+
/**
202+
* A view-class route-setup from either:
203+
* - `add_view` method on a `aiohttp.web.UrlDispatcher`
204+
* - `view` function from `aiohttp.web`
205+
*/
206+
class AiohttpViewRouteSetupFromFunction extends AiohttpViewRouteSetup, DataFlow::CallCfgNode {
207+
AiohttpViewRouteSetupFromFunction() {
208+
this = urlDispathcerInstance().getMember("add_view").getACall()
209+
or
210+
this = API::moduleImport("aiohttp").getMember("web").getMember("view").getACall()
211+
or
212+
this =
213+
API::moduleImport("aiohttp")
214+
.getMember("web")
215+
.getMember("RouteTableDef")
216+
.getReturn()
217+
.getMember("view")
218+
.getACall()
219+
}
220+
221+
override DataFlow::Node getUrlPatternArg() {
222+
result in [this.getArg(0), this.getArgByName("path")]
223+
}
224+
225+
override DataFlow::Node getViewClassArg() {
226+
result in [this.getArg(1), this.getArgByName("handler")]
227+
}
228+
}
229+
230+
/**
231+
* A view-class route-setup from the `view` decorator from a `aiohttp.web.RouteTableDef`.
232+
*/
233+
class AiohttpViewRouteSetupFromDecorator extends AiohttpViewRouteSetup, DataFlow::CallCfgNode {
234+
AiohttpViewRouteSetupFromDecorator() {
235+
this =
236+
API::moduleImport("aiohttp")
237+
.getMember("web")
238+
.getMember("RouteTableDef")
239+
.getReturn()
240+
.getMember("view")
241+
.getACall()
242+
}
243+
244+
override DataFlow::Node getUrlPatternArg() {
245+
result in [this.getArg(0), this.getArgByName("path")]
246+
}
247+
248+
override DataFlow::Node getViewClassArg() { none() }
249+
250+
override Class getViewClass() { result.getADecorator() = this.asExpr() }
251+
}
252+
253+
/** A class that we consider a aiohttp.web View class. */
254+
abstract class AiohttpViewClass extends Class {
255+
/** Gets a function that could handle incoming requests, if any. */
256+
Function getARequestHandler() {
257+
// TODO: This doesn't handle attribute assignment. Should be OK, but analysis is not as complete as with
258+
// points-to and `.lookup`, which would handle `post = my_post_handler` inside class def
259+
result = this.getAMethod() and
260+
result.getName() = HTTP::httpVerbLower()
261+
}
262+
}
263+
264+
/** A class that has a super-type which is a aiohttp.web View class. */
265+
class AiohttpViewClassFromSuperClass extends AiohttpViewClass {
266+
AiohttpViewClassFromSuperClass() { this.getABase() = View::subclassRef().getAUse().asExpr() }
267+
}
268+
269+
/** A class that is used in a route-setup, therefore being considered a aiohttp.web View class. */
270+
class AiohttpViewClassFromRouteSetup extends AiohttpViewClass {
271+
AiohttpViewClassFromRouteSetup() { this = any(AiohttpViewRouteSetup rs).getViewClass() }
272+
}
273+
274+
/** A request handler defined in an `aiohttp.web` view class, that has no known route. */
275+
private class AiohttpViewClassRequestHandlerWithoutKnownRoute extends HTTP::Server::RequestHandler::Range {
276+
AiohttpViewClassRequestHandlerWithoutKnownRoute() {
277+
exists(AiohttpViewClass vc | vc.getARequestHandler() = this) and
278+
not exists(AiohttpRouteSetup setup | setup.getARequestHandler() = this)
279+
}
280+
281+
override Parameter getARoutedParameter() { none() }
282+
283+
override string getFramework() { result = "aiohttp.web" }
284+
}
285+
145286
// ---------------------------------------------------------------------------
146287
// aiohttp.web.Request taint modeling
147288
// ---------------------------------------------------------------------------
@@ -183,7 +324,7 @@ module AiohttpWebModel {
183324
DataFlow::ParameterNode {
184325
AiohttpRequestHandlerRequestParam() {
185326
exists(Function requestHandler |
186-
requestHandler = any(AiohttpRouteSetup setup).getARequestHandler() and
327+
requestHandler = any(AiohttpCoroutineRouteSetup setup).getARequestHandler() and
187328
// We select the _last_ parameter for the request since that is what they do in
188329
// `aiohttp-jinja2`.
189330
// https://github.com/aio-libs/aiohttp-jinja2/blob/7fb4daf2c3003921d34031d38c2311ee0e02c18b/aiohttp_jinja2/__init__.py#L235
@@ -200,6 +341,11 @@ module AiohttpWebModel {
200341
this.getParameter() =
201342
max(Parameter param, int i | param = requestHandler.getArg(i) | param order by i)
202343
)
344+
or
345+
exists(AiohttpViewClass vc |
346+
// TODO
347+
none()
348+
)
203349
}
204350

205351
override string getSourceType() { result = "aiohttp.web.Request" }

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

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -83,44 +83,44 @@ async def foo_handler(self, request): # $ MISSING: requestHandler
8383

8484
# `app.add_routes` with list
8585
class MyWebView1(web.View):
86-
async def get(self): # $ MISSING: requestHandler
86+
async def get(self): # $ requestHandler
8787
return web.Response(text="MyWebView1.get")
8888

8989
app.add_routes([
90-
web.view("/MyWebView1", MyWebView1) # $ MISSING: routeSetup
90+
web.view("/MyWebView1", MyWebView1) # $ routeSetup="/MyWebView1"
9191
])
9292

9393

9494
# using decorator
9595
routes = web.RouteTableDef()
9696

97-
@routes.view("/MyWebView2") # $ MISSING: routeSetup
97+
@routes.view("/MyWebView2") # $ routeSetup="/MyWebView2"
9898
class MyWebView2(web.View):
99-
async def get(self): # $ MISSING: requestHandler
99+
async def get(self): # $ requestHandler
100100
return web.Response(text="MyWebView2.get")
101101

102102
app.add_routes(routes)
103103

104104

105105
# `app.router.add_view`
106106
class MyWebView3(web.View):
107-
async def get(self): # $ MISSING: requestHandler
107+
async def get(self): # $ requestHandler
108108
return web.Response(text="MyWebView3.get")
109109

110-
app.router.add_view("/MyWebView3", MyWebView3) # $ MISSING: routeSetup
110+
app.router.add_view("/MyWebView3", MyWebView3) # $ routeSetup="/MyWebView3"
111111

112112
# no route-setup
113113
class MyWebViewNoRoute(web.View):
114-
async def get(self): # $ MISSING: requestHandler
114+
async def get(self): # $ requestHandler
115115
return web.Response(text="MyWebViewNoRoute.get")
116116

117117
if len(__name__) < 0: # avoid running, but fool analysis to not consider dead code
118118
# no explicit-view subclass (but route-setup)
119119
class MyWebViewNoSubclassButRoute(somelib.someclass):
120-
async def get(self): # $ MISSING: requestHandler
120+
async def get(self): # $ requestHandler
121121
return web.Response(text="MyWebViewNoSubclassButRoute.get")
122122

123-
app.router.add_view("/MyWebViewNoSubclassButRoute", MyWebViewNoSubclassButRoute) # $ MISSING: routeSetup
123+
app.router.add_view("/MyWebViewNoSubclassButRoute", MyWebViewNoSubclassButRoute) # $ routeSetup="/MyWebViewNoSubclassButRoute"
124124

125125
## =================== ##
126126
## "Routed parameters" ##

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ async def test_taint(request: web.Request): # $ requestHandler
194194

195195

196196
class TaintTestClass(web.View):
197-
def get(self):
197+
def get(self): # $ requestHandler
198198
ensure_tainted(
199199
self.request, # $ MISSING: tainted
200200
)

0 commit comments

Comments
 (0)