Skip to content

Commit 3cbb909

Browse files
committed
Python: Add modeling of coroutine routes in aiohttp.web
1 parent fa1d4e6 commit 3cbb909

File tree

4 files changed

+168
-25
lines changed

4 files changed

+168
-25
lines changed

docs/codeql/support/reusables/frameworks.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ Python built-in support
152152
:widths: auto
153153

154154
Name, Category
155+
aiohttp.web, Web framework
155156
Django, Web framework
156157
Flask, Web framework
157158
Tornado, Web framework

python/ql/src/semmle/python/Frameworks.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
// If you add modeling of a new framework/library, remember to add it it to the docs in
66
// `docs/codeql/support/reusables/frameworks.rst`
7+
private import semmle.python.frameworks.Aiohttp
78
private import semmle.python.frameworks.Cryptodome
89
private import semmle.python.frameworks.Cryptography
910
private import semmle.python.frameworks.Dill
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/**
2+
* Provides classes modeling security-relevant aspects of the `aiohttp` PyPI package.
3+
* See https://docs.aiohttp.org/en/stable/index.html
4+
*/
5+
6+
private import python
7+
private import semmle.python.dataflow.new.DataFlow
8+
private import semmle.python.Concepts
9+
private import semmle.python.ApiGraphs
10+
private import semmle.python.frameworks.internal.PoorMansFunctionResolution
11+
12+
/**
13+
* INTERNAL: Do not use.
14+
*
15+
* Provides models for the web server part (`aiohttp.web`) of the `aiohttp` PyPI package.
16+
* See https://docs.aiohttp.org/en/stable/web.html
17+
*/
18+
module AiohttpWebModel {
19+
/** Gets a reference to a `aiohttp.web.Application` instance. */
20+
API::Node applicationInstance() {
21+
// Not sure whether you're allowed to add routes _after_ starting the app, for
22+
// example in the middle of handling a http request... but I'm guessing that for 99%
23+
// for all code, not modeling that `request.app` is a reference to an application
24+
// should be good enough for the route-setup part of the modeling :+1:
25+
result = API::moduleImport("aiohttp").getMember("web").getMember("Application").getReturn()
26+
}
27+
28+
/** Gets a reference to a `aiohttp.web.UrlDispatcher` instance. */
29+
API::Node urlDispathcerInstance() {
30+
result = API::moduleImport("aiohttp").getMember("web").getMember("UrlDispatcher").getReturn()
31+
or
32+
result = applicationInstance().getMember("router")
33+
}
34+
35+
// -- route modeling --
36+
37+
/** A route setup in `aiohttp.web` */
38+
abstract class AiohttpRouteSetup extends HTTP::Server::RouteSetup::Range {
39+
override Parameter getARoutedParameter() { none() }
40+
41+
override string getFramework() { result = "aiohttp.web" }
42+
}
43+
44+
/** An aiohttp route setup that uses coroutines (async function) as request handler. */
45+
abstract class AiohttpCoroutineRouteSetup extends AiohttpRouteSetup {
46+
/** Gets the argument specifying the request handler (which is a coroutine/async function) */
47+
abstract DataFlow::Node getRequestHandlerArg();
48+
49+
override Function getARequestHandler() {
50+
this.getRequestHandlerArg() = poorMansFunctionTracker(result)
51+
}
52+
}
53+
54+
/**
55+
* A route-setup from `add_route` or any of `add_get`, `add_post`, etc. on an
56+
* `aiohttp.web.UrlDispatcher`.
57+
*/
58+
class AiohttpUrlDispatcherAddRouteCall extends AiohttpCoroutineRouteSetup, DataFlow::CallCfgNode {
59+
/** At what index route arguments starts, so we can handle "route" version together with get/post/... */
60+
int routeArgsStart;
61+
62+
AiohttpUrlDispatcherAddRouteCall() {
63+
this = urlDispathcerInstance().getMember("add_" + HTTP::httpVerbLower()).getACall() and
64+
routeArgsStart = 0
65+
or
66+
this = urlDispathcerInstance().getMember("add_route").getACall() and
67+
routeArgsStart = 1
68+
}
69+
70+
override DataFlow::Node getUrlPatternArg() {
71+
result in [this.getArg(routeArgsStart + 0), this.getArgByName("path")]
72+
}
73+
74+
override DataFlow::Node getRequestHandlerArg() {
75+
result in [this.getArg(routeArgsStart + 1), this.getArgByName("handler")]
76+
}
77+
}
78+
79+
/**
80+
* A route-setup from using `route` or any of `get`, `post`, etc. functions from `aiohttp.web`.
81+
*
82+
* Note that technically, this does not set up a route in itself (since it needs to be added to an application first).
83+
* However, modeling this way makes it easier, and we don't expect it to lead to many problems.
84+
*/
85+
class AiohttpWebRouteCall extends AiohttpCoroutineRouteSetup, DataFlow::CallCfgNode {
86+
/** At what index route arguments starts, so we can handle "route" version together with get/post/... */
87+
int routeArgsStart;
88+
89+
AiohttpWebRouteCall() {
90+
exists(string funcName |
91+
funcName = HTTP::httpVerbLower() and
92+
routeArgsStart = 0
93+
or
94+
funcName = "route" and
95+
routeArgsStart = 1
96+
|
97+
this = API::moduleImport("aiohttp").getMember("web").getMember(funcName).getACall()
98+
)
99+
}
100+
101+
override DataFlow::Node getUrlPatternArg() {
102+
result in [this.getArg(routeArgsStart + 0), this.getArgByName("path")]
103+
}
104+
105+
override DataFlow::Node getRequestHandlerArg() {
106+
result in [this.getArg(routeArgsStart + 1), this.getArgByName("handler")]
107+
}
108+
}
109+
110+
/** A route-setup from using a `route` or any of `get`, `post`, etc. decorators from a `aiohttp.web.RouteTableDef`. */
111+
class AiohttpRouteTableDefRouteCall extends AiohttpCoroutineRouteSetup, DataFlow::CallCfgNode {
112+
/** At what index route arguments starts, so we can handle "route" version together with get/post/... */
113+
int routeArgsStart;
114+
115+
AiohttpRouteTableDefRouteCall() {
116+
exists(string decoratorName |
117+
decoratorName = HTTP::httpVerbLower() and
118+
routeArgsStart = 0
119+
or
120+
decoratorName = "route" and
121+
routeArgsStart = 1
122+
|
123+
this =
124+
API::moduleImport("aiohttp")
125+
.getMember("web")
126+
.getMember("RouteTableDef")
127+
.getReturn()
128+
.getMember(decoratorName)
129+
.getACall()
130+
)
131+
}
132+
133+
override DataFlow::Node getUrlPatternArg() {
134+
result in [this.getArg(routeArgsStart + 0), this.getArgByName("path")]
135+
}
136+
137+
override DataFlow::Node getRequestHandlerArg() { none() }
138+
139+
override Function getARequestHandler() { result.getADecorator() = this.asExpr() }
140+
}
141+
}

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

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -15,55 +15,55 @@
1515
if True:
1616

1717
# `app.add_routes` with list
18-
async def foo(request): # $ MISSING: requestHandler
18+
async def foo(request): # $ requestHandler
1919
return web.Response(text="foo")
2020

21-
async def foo2(request): # $ MISSING: requestHandler
21+
async def foo2(request): # $ requestHandler
2222
return web.Response(text="foo2")
2323

24-
async def foo3(request): # $ MISSING: requestHandler
24+
async def foo3(request): # $ requestHandler
2525
return web.Response(text="foo3")
2626

2727
app.add_routes([
28-
web.get("/foo", foo), # $ MISSING: routeSetup
29-
web.route("*", "/foo2", foo2), # $ MISSING: routeSetup
30-
web.get(path="/foo3", handler=foo3), # $ MISSING: routeSetup
28+
web.get("/foo", foo), # $ routeSetup="/foo"
29+
web.route("*", "/foo2", foo2), # $ routeSetup="/foo2"
30+
web.get(path="/foo3", handler=foo3), # $ routeSetup="/foo3"
3131
])
3232

3333

3434
# using decorator
3535
routes = web.RouteTableDef()
3636

37-
@routes.get("/bar") # $ MISSING: routeSetup
38-
async def bar(request): # $ MISSING: requestHandler
37+
@routes.get("/bar") # $ routeSetup="/bar"
38+
async def bar(request): # $ requestHandler
3939
return web.Response(text="bar")
4040

41-
@routes.route("*", "/bar2") # $ MISSING: routeSetup
42-
async def bar2(request): # $ MISSING: requestHandler
41+
@routes.route("*", "/bar2") # $ routeSetup="/bar2"
42+
async def bar2(request): # $ requestHandler
4343
return web.Response(text="bar2")
4444

45-
@routes.get(path="/bar3") # $ MISSING: routeSetup
46-
async def bar3(request): # $ MISSING: requestHandler
45+
@routes.get(path="/bar3") # $ routeSetup="/bar3"
46+
async def bar3(request): # $ requestHandler
4747
return web.Response(text="bar3")
4848

4949
app.add_routes(routes)
5050

5151

5252
# `app.router.add_get` / `app.router.add_route`
53-
async def baz(request): # $ MISSING: requestHandler
53+
async def baz(request): # $ requestHandler
5454
return web.Response(text="baz")
5555

56-
app.router.add_get("/baz", baz) # $ MISSING: routeSetup
56+
app.router.add_get("/baz", baz) # $ routeSetup="/baz"
5757

58-
async def baz2(request): # $ MISSING: requestHandler
58+
async def baz2(request): # $ requestHandler
5959
return web.Response(text="baz2")
6060

61-
app.router.add_route("*", "/baz2", baz2) # $ MISSING: routeSetup
61+
app.router.add_route("*", "/baz2", baz2) # $ routeSetup="/baz2"
6262

63-
async def baz3(request): # $ MISSING: requestHandler
63+
async def baz3(request): # $ requestHandler
6464
return web.Response(text="baz3")
6565

66-
app.router.add_get(path="/baz3", handler=baz3) # $ MISSING: routeSetup
66+
app.router.add_get(path="/baz3", handler=baz3) # $ routeSetup="/baz3"
6767

6868

6969
## Using classes / views
@@ -76,7 +76,7 @@ async def foo_handler(self, request): # $ MISSING: requestHandler
7676
return web.Response(text="MyCustomHandlerClass.foo")
7777

7878
my_custom_handler = MyCustomHandlerClass()
79-
app.router.add_get("/MyCustomHandlerClass/foo", my_custom_handler.foo_handler) # $ MISSING: routeSetup
79+
app.router.add_get("/MyCustomHandlerClass/foo", my_custom_handler.foo_handler) # $ routeSetup="/MyCustomHandlerClass/foo"
8080

8181
# Using `web.View`
8282
# ---------------
@@ -116,12 +116,12 @@ async def get(self): # $ MISSING: requestHandler
116116
if True:
117117
# see https://docs.aiohttp.org/en/stable/web_quickstart.html#variable-resources
118118

119-
async def matching(request: web.Request): # $ MISSING: requestHandler
119+
async def matching(request: web.Request): # $ requestHandler
120120
name = request.match_info['name']
121121
number = request.match_info['number']
122122
return web.Response(text="matching name={} number={}".format(name, number))
123123

124-
app.router.add_get("/matching/{name}/{number:\d+}", matching) # $ MISSING: routeSetup
124+
app.router.add_get(r"/matching/{name}/{number:\d+}", matching) # $ routeSetup="/matching/{name}/{number:\d+}"
125125

126126
## ======= ##
127127
## subapps ##
@@ -130,10 +130,10 @@ async def matching(request: web.Request): # $ MISSING: requestHandler
130130
if True:
131131
subapp = web.Application()
132132

133-
async def subapp_handler(request): # $ MISSING: requestHandler
133+
async def subapp_handler(request): # $ requestHandler
134134
return web.Response(text="subapp_handler")
135135

136-
subapp.router.add_get("/subapp_handler", subapp_handler) # $ MISSING: routeSetup
136+
subapp.router.add_get("/subapp_handler", subapp_handler) # $ routeSetup="/subapp_handler"
137137

138138
app.add_subapp("/my_subapp", subapp)
139139

@@ -146,11 +146,11 @@ async def subapp_handler(request): # $ MISSING: requestHandler
146146
## ================================ ##
147147

148148
if True:
149-
async def manual_dispatcher_instance(request): # $ MISSING: requestHandler
149+
async def manual_dispatcher_instance(request): # $ requestHandler
150150
return web.Response(text="manual_dispatcher_instance")
151151

152152
url_dispatcher = web.UrlDispatcher()
153-
url_dispatcher.add_get("/manual_dispatcher_instance", manual_dispatcher_instance) # $ MISSING: routeSetup
153+
url_dispatcher.add_get("/manual_dispatcher_instance", manual_dispatcher_instance) # $ routeSetup="/manual_dispatcher_instance"
154154

155155
subapp2 = web.Application(router=url_dispatcher)
156156
app.add_subapp("/manual_dispatcher_instance_app", subapp2)

0 commit comments

Comments
 (0)