Skip to content

Commit 51a0528

Browse files
authored
Merge pull request github#13731 from pwntester/py/aiohttp_improvements
Python: Aiohttp improvements
2 parents 01ff690 + f865fa3 commit 51a0528

File tree

7 files changed

+141
-9
lines changed

7 files changed

+141
-9
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Improvements of the `aiohttp` models including remote-flow-sources from type annotations, new path manipulation, and SSRF sinks.

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

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,27 @@ module AiohttpWebModel {
468468
override string getSourceType() { result = "aiohttp.web.Request" }
469469
}
470470

471+
/**
472+
* A parameter that has a type annotation of `aiohttp.web.Request`, so with all
473+
* likelihood will receive an `aiohttp.web.Request` instance at some point when a
474+
* request handler is invoked.
475+
*/
476+
class AiohttpRequestParamFromTypeAnnotation extends Request::InstanceSource,
477+
DataFlow::ParameterNode, RemoteFlowSource::Range
478+
{
479+
AiohttpRequestParamFromTypeAnnotation() {
480+
not this instanceof AiohttpRequestHandlerRequestParam and
481+
this.getParameter().getAnnotation() =
482+
API::moduleImport("aiohttp")
483+
.getMember("web")
484+
.getMember("Request")
485+
.getAValueReachableFromSource()
486+
.asExpr()
487+
}
488+
489+
override string getSourceType() { result = "aiohttp.web.Request from type-annotation" }
490+
}
491+
471492
/**
472493
* A read of the `request` attribute on an instance of an aiohttp.web View class,
473494
* which is the request being processed currently.
@@ -498,14 +519,17 @@ module AiohttpWebModel {
498519
* - https://docs.aiohttp.org/en/stable/web_quickstart.html#aiohttp-web-exceptions
499520
*/
500521
class AiohttpWebResponseInstantiation extends Http::Server::HttpResponse::Range,
501-
Response::InstanceSource, DataFlow::CallCfgNode
522+
Response::InstanceSource, API::CallNode
502523
{
503524
API::Node apiNode;
504525

505526
AiohttpWebResponseInstantiation() {
506527
this = apiNode.getACall() and
507528
(
508-
apiNode = API::moduleImport("aiohttp").getMember("web").getMember("Response")
529+
apiNode =
530+
API::moduleImport("aiohttp")
531+
.getMember("web")
532+
.getMember(["FileResponse", "Response", "StreamResponse"])
509533
or
510534
exists(string httpExceptionClassName |
511535
httpExceptionClassName in [
@@ -545,6 +569,10 @@ module AiohttpWebModel {
545569

546570
override DataFlow::Node getMimetypeOrContentTypeArg() {
547571
result = this.getArgByName("content_type")
572+
or
573+
exists(string key | key.toLowerCase() = "content-type" |
574+
result = this.getKeywordParameter("headers").getSubscript(key).getAValueReachingSink()
575+
)
548576
}
549577

550578
override string getMimetypeDefault() {
@@ -556,6 +584,37 @@ module AiohttpWebModel {
556584
}
557585
}
558586

587+
/**
588+
* A call to the `aiohttp.web.FileResponse` constructor as a sink for Filesystem access.
589+
*/
590+
class FileResponseCall extends FileSystemAccess::Range, API::CallNode {
591+
FileResponseCall() {
592+
this = API::moduleImport("aiohttp").getMember("web").getMember("FileResponse").getACall()
593+
}
594+
595+
override DataFlow::Node getAPathArgument() { result = this.getParameter(0, "path").asSink() }
596+
}
597+
598+
/**
599+
* An instantiation of `aiohttp.web.StreamResponse`.
600+
*
601+
* See https://docs.aiohttp.org/en/stable/web_reference.html#aiohttp.web.StreamResponse
602+
*/
603+
class StreamResponse extends AiohttpWebResponseInstantiation {
604+
StreamResponse() {
605+
this = API::moduleImport("aiohttp").getMember("web").getMember("StreamResponse").getACall()
606+
}
607+
608+
override DataFlow::Node getBody() {
609+
result =
610+
this.getReturn()
611+
.getMember(["write", "write_eof"])
612+
.getACall()
613+
.getParameter(0, "data")
614+
.asSink()
615+
}
616+
}
617+
559618
/** Gets an HTTP response instance. */
560619
private API::Node aiohttpResponseInstance() {
561620
result = any(AiohttpWebResponseInstantiation call).getApiNode().getReturn()
@@ -670,14 +729,14 @@ private module AiohttpClientModel {
670729
string methodName;
671730

672731
OutgoingRequestCall() {
673-
methodName in [Http::httpVerbLower(), "request"] and
732+
methodName in [Http::httpVerbLower(), "request", "ws_connect"] and
674733
this = instance().getMember(methodName).getACall()
675734
}
676735

677736
override DataFlow::Node getAUrlPart() {
678737
result = this.getArgByName("url")
679738
or
680-
not methodName = "request" and
739+
methodName in [Http::httpVerbLower(), "ws_connect"] and
681740
result = this.getArg(0)
682741
or
683742
methodName = "request" and

python/ql/test/experimental/meta/ConceptsTest.qll

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -292,10 +292,11 @@ module HttpServerHttpResponseTest implements TestSig {
292292
exists(DedicatedResponseTest d | d.isDedicatedFile(file))
293293
) and
294294
(
295-
exists(Http::Server::HttpResponse response |
296-
location = response.getLocation() and
297-
element = response.toString() and
298-
value = prettyNodeForInlineTest(response.getBody()) and
295+
exists(Http::Server::HttpResponse response, DataFlow::Node body |
296+
body = response.getBody() and
297+
location = body.getLocation() and
298+
element = body.toString() and
299+
value = prettyNodeForInlineTest(body) and
299300
tag = "responseBody"
300301
)
301302
or
Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,19 @@
11
import experimental.meta.InlineTaintTest
2-
import MakeInlineTaintTest<TestTaintTrackingConfig>
2+
3+
predicate isSafe(DataFlow::GuardNode g, ControlFlowNode node, boolean branch) {
4+
g.(CallNode).getFunction().(NameNode).getId() = "is_safe" and
5+
node = g.(CallNode).getArg(_) and
6+
branch = true
7+
}
8+
9+
module CustomSanitizerOverridesConfig implements DataFlow::ConfigSig {
10+
predicate isSource = TestTaintTrackingConfig::isSource/1;
11+
12+
predicate isSink = TestTaintTrackingConfig::isSink/1;
13+
14+
predicate isBarrier(DataFlow::Node node) {
15+
node = DataFlow::BarrierGuard<isSafe/3>::getABarrierNode()
16+
}
17+
}
18+
19+
import MakeInlineTaintTest<CustomSanitizerOverridesConfig>

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,5 @@ async def test():
3333
assert context.verify_mode == ssl.VerifyMode.CERT_NONE
3434

3535
s.get("url", ssl=context) # $ clientRequestUrlPart="url" MISSING: clientRequestCertValidationDisabled
36+
37+
s.ws_connect("url") # $ clientRequestUrlPart="url"

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ async def html_text(request): # $ requestHandler
2323
async def html_body(request): # $ requestHandler
2424
return web.Response(body=b"foo", content_type="text/html") # $ HttpResponse mimetype=text/html responseBody=b"foo"
2525

26+
@routes.get("/html_body_header") # $ routeSetup="/html_body_header"
27+
async def html_body_header(request): # $ requestHandler
28+
return web.Response(headers={"content-type": "text/html"}, text="foo") # $ HttpResponse mimetype=text/html responseBody="foo"
2629

2730
@routes.get("/html_body_set_later") # $ routeSetup="/html_body_set_later"
2831
async def html_body_set_later(request): # $ requestHandler
@@ -65,6 +68,26 @@ async def redirect_302(request): # $ requestHandler
6568
else:
6669
raise web.HTTPFound(location="/logout") # $ HttpResponse HttpRedirectResponse mimetype=application/octet-stream redirectLocation="/logout"
6770

71+
72+
@routes.get("/file_response") # $ routeSetup="/file_response"
73+
async def file_response(request): # $ requestHandler
74+
filename = "foo.txt"
75+
resp = web.FileResponse(filename) # $ HttpResponse mimetype=application/octet-stream getAPathArgument=filename
76+
resp = web.FileResponse(path=filename) # $ HttpResponse mimetype=application/octet-stream getAPathArgument=filename
77+
return resp
78+
79+
80+
@routes.get("/streaming_response") # $ routeSetup="/streaming_response"
81+
async def streaming_response(request): # $ requestHandler
82+
resp = web.StreamResponse() # $ HttpResponse mimetype=application/octet-stream
83+
await resp.prepare(request)
84+
85+
await resp.write(b"foo") # $ responseBody=b"foo"
86+
await resp.write(data=b"bar") # $ responseBody=b"bar"
87+
await resp.write_eof(b"baz") # $ responseBody=b"baz"
88+
89+
return resp
90+
6891
################################################################################
6992
# Cookies
7093
################################################################################

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,10 +142,36 @@ def get(self): # $ requestHandler
142142
self.request.url # $ tainted
143143
)
144144

145+
# not a request handler, and not called, but since we have type-annotation, should be a
146+
# remote-flow-source.
147+
async def test_source_from_type_annotation(request: web.Request):
148+
# picking out just a few of the tests from `test_taint` above, to show that we have
149+
# the same taint-steps :)
150+
ensure_tainted(
151+
request, # $ tainted
152+
request.url, # $ tainted
153+
await request.content.read(), # $ tainted
154+
)
155+
156+
# Test that since we can reach the `request` object in the helper function, we don't
157+
# introduce a new remote-flow-source, but instead use the one from the caller. (which is
158+
# checked to not be tainted)
159+
async def test_sanitizer(request): # $ requestHandler
160+
ensure_tainted(request, request.url, await request.content.read()) # $ tainted
161+
162+
if (is_safe(request)):
163+
ensure_not_tainted(request, request.url, await request.content.read())
164+
test_safe_helper_function_no_route_with_type(request)
165+
166+
167+
async def test_safe_helper_function_no_route_with_type(request: web.Request):
168+
ensure_not_tainted(request, request.url, await request.content.read()) # $ SPURIOUS: tainted
169+
145170

146171
app = web.Application()
147172
app.router.add_get(r"/test_taint/{name}/{number:\d+}", test_taint) # $ routeSetup="/test_taint/{name}/{number:\d+}"
148173
app.router.add_view(r"/test_taint_class", TaintTestClass) # $ routeSetup="/test_taint_class"
174+
app.router.add_view(r"/test_sanitizer", test_sanitizer) # $ routeSetup="/test_sanitizer"
149175

150176

151177
if __name__ == "__main__":

0 commit comments

Comments
 (0)