Skip to content

Commit 226425e

Browse files
committed
Python: Model CookieWrite for aiohttp
1 parent e1af1f1 commit 226425e

File tree

2 files changed

+133
-29
lines changed

2 files changed

+133
-29
lines changed

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

Lines changed: 129 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,36 @@ module AiohttpWebModel {
295295
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
296296
}
297297

298+
/**
299+
* Provides models for the `aiohttp.web.Response` class
300+
*
301+
* See https://docs.aiohttp.org/en/stable/web_reference.html#response-classes
302+
*/
303+
module Response {
304+
/**
305+
* A source of instances of `aiohttp.web.Response`, extend this class to model new instances.
306+
*
307+
* This can include instantiations of the class, return values from function
308+
* calls, or a special parameter that will be set when functions are called by an external
309+
* library.
310+
*
311+
* Use `Response::instance()` predicate to get
312+
* references to instances of `aiohttp.web.Response`.
313+
*/
314+
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
315+
316+
/** Gets a reference to an instance of `aiohttp.web.Response`. */
317+
private DataFlow::LocalSourceNode instance(DataFlow::TypeTracker t) {
318+
t.start() and
319+
result instanceof InstanceSource
320+
or
321+
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
322+
}
323+
324+
/** Gets a reference to an instance of `aiohttp.web.Response`. */
325+
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
326+
}
327+
298328
/**
299329
* Provides models for the `aiohttp.StreamReader` class
300330
*
@@ -488,35 +518,46 @@ module AiohttpWebModel {
488518
* - https://docs.aiohttp.org/en/stable/web_quickstart.html#aiohttp-web-exceptions
489519
*/
490520
class AiohttpWebResponseInstantiation extends HTTP::Server::HttpResponse::Range,
491-
DataFlow::CallCfgNode {
521+
Response::InstanceSource, DataFlow::CallCfgNode {
522+
API::Node apiNode;
523+
492524
AiohttpWebResponseInstantiation() {
493-
this = API::moduleImport("aiohttp").getMember("web").getMember("Response").getACall()
494-
or
495-
exists(string httpExceptionClassName |
496-
httpExceptionClassName in [
497-
"HTTPException", "HTTPSuccessful", "HTTPOk", "HTTPCreated", "HTTPAccepted",
498-
"HTTPNonAuthoritativeInformation", "HTTPNoContent", "HTTPResetContent",
499-
"HTTPPartialContent", "HTTPRedirection", "HTTPMultipleChoices", "HTTPMovedPermanently",
500-
"HTTPFound", "HTTPSeeOther", "HTTPNotModified", "HTTPUseProxy", "HTTPTemporaryRedirect",
501-
"HTTPPermanentRedirect", "HTTPError", "HTTPClientError", "HTTPBadRequest",
502-
"HTTPUnauthorized", "HTTPPaymentRequired", "HTTPForbidden", "HTTPNotFound",
503-
"HTTPMethodNotAllowed", "HTTPNotAcceptable", "HTTPProxyAuthenticationRequired",
504-
"HTTPRequestTimeout", "HTTPConflict", "HTTPGone", "HTTPLengthRequired",
505-
"HTTPPreconditionFailed", "HTTPRequestEntityTooLarge", "HTTPRequestURITooLong",
506-
"HTTPUnsupportedMediaType", "HTTPRequestRangeNotSatisfiable", "HTTPExpectationFailed",
507-
"HTTPMisdirectedRequest", "HTTPUnprocessableEntity", "HTTPFailedDependency",
508-
"HTTPUpgradeRequired", "HTTPPreconditionRequired", "HTTPTooManyRequests",
509-
"HTTPRequestHeaderFieldsTooLarge", "HTTPUnavailableForLegalReasons", "HTTPServerError",
510-
"HTTPInternalServerError", "HTTPNotImplemented", "HTTPBadGateway",
511-
"HTTPServiceUnavailable", "HTTPGatewayTimeout", "HTTPVersionNotSupported",
512-
"HTTPVariantAlsoNegotiates", "HTTPInsufficientStorage", "HTTPNotExtended",
513-
"HTTPNetworkAuthenticationRequired"
514-
] and
515-
this =
516-
API::moduleImport("aiohttp").getMember("web").getMember(httpExceptionClassName).getACall()
525+
this = apiNode.getACall() and
526+
(
527+
apiNode = API::moduleImport("aiohttp").getMember("web").getMember("Response")
528+
or
529+
exists(string httpExceptionClassName |
530+
httpExceptionClassName in [
531+
"HTTPException", "HTTPSuccessful", "HTTPOk", "HTTPCreated", "HTTPAccepted",
532+
"HTTPNonAuthoritativeInformation", "HTTPNoContent", "HTTPResetContent",
533+
"HTTPPartialContent", "HTTPRedirection", "HTTPMultipleChoices",
534+
"HTTPMovedPermanently", "HTTPFound", "HTTPSeeOther", "HTTPNotModified",
535+
"HTTPUseProxy", "HTTPTemporaryRedirect", "HTTPPermanentRedirect", "HTTPError",
536+
"HTTPClientError", "HTTPBadRequest", "HTTPUnauthorized", "HTTPPaymentRequired",
537+
"HTTPForbidden", "HTTPNotFound", "HTTPMethodNotAllowed", "HTTPNotAcceptable",
538+
"HTTPProxyAuthenticationRequired", "HTTPRequestTimeout", "HTTPConflict", "HTTPGone",
539+
"HTTPLengthRequired", "HTTPPreconditionFailed", "HTTPRequestEntityTooLarge",
540+
"HTTPRequestURITooLong", "HTTPUnsupportedMediaType", "HTTPRequestRangeNotSatisfiable",
541+
"HTTPExpectationFailed", "HTTPMisdirectedRequest", "HTTPUnprocessableEntity",
542+
"HTTPFailedDependency", "HTTPUpgradeRequired", "HTTPPreconditionRequired",
543+
"HTTPTooManyRequests", "HTTPRequestHeaderFieldsTooLarge",
544+
"HTTPUnavailableForLegalReasons", "HTTPServerError", "HTTPInternalServerError",
545+
"HTTPNotImplemented", "HTTPBadGateway", "HTTPServiceUnavailable",
546+
"HTTPGatewayTimeout", "HTTPVersionNotSupported", "HTTPVariantAlsoNegotiates",
547+
"HTTPInsufficientStorage", "HTTPNotExtended", "HTTPNetworkAuthenticationRequired"
548+
] and
549+
apiNode = API::moduleImport("aiohttp").getMember("web").getMember(httpExceptionClassName)
550+
)
517551
)
518552
}
519553

554+
/**
555+
* INTERNAL: Do not use.
556+
*
557+
* Get the internal `API::Node` that this is call of.
558+
*/
559+
API::Node getApiNode() { result = apiNode }
560+
520561
override DataFlow::Node getBody() {
521562
result in [this.getArgByName("text"), this.getArgByName("body")]
522563
}
@@ -534,6 +575,11 @@ module AiohttpWebModel {
534575
}
535576
}
536577

578+
/** Gets an HTTP response instance. */
579+
private API::Node aiohttpResponseInstance() {
580+
result = any(AiohttpWebResponseInstantiation call).getApiNode().getReturn()
581+
}
582+
537583
/**
538584
* An instantiation of aiohttp.web HTTP redirect exception.
539585
*
@@ -559,4 +605,62 @@ module AiohttpWebModel {
559605
result in [this.getArg(0), this.getArgByName("location")]
560606
}
561607
}
608+
609+
/**
610+
* A call to `set_cookie` on a HTTP Response.
611+
*/
612+
class AiohttpResponseSetCookieCall extends HTTP::Server::CookieWrite::Range, DataFlow::CallCfgNode {
613+
AiohttpResponseSetCookieCall() {
614+
this = aiohttpResponseInstance().getMember("set_cookie").getACall()
615+
}
616+
617+
override DataFlow::Node getHeaderArg() { none() }
618+
619+
override DataFlow::Node getNameArg() { result in [this.getArg(0), this.getArgByName("name")] }
620+
621+
override DataFlow::Node getValueArg() { result in [this.getArg(1), this.getArgByName("value")] }
622+
}
623+
624+
/**
625+
* A call to `del_cookie` on a HTTP Response.
626+
*/
627+
class AiohttpResponseDelCookieCall extends HTTP::Server::CookieWrite::Range, DataFlow::CallCfgNode {
628+
AiohttpResponseDelCookieCall() {
629+
this = aiohttpResponseInstance().getMember("del_cookie").getACall()
630+
}
631+
632+
override DataFlow::Node getHeaderArg() { none() }
633+
634+
override DataFlow::Node getNameArg() { result in [this.getArg(0), this.getArgByName("name")] }
635+
636+
override DataFlow::Node getValueArg() { none() }
637+
}
638+
639+
/**
640+
* A dict-like write to an item of the `cookies` attribute on a HTTP response, such as
641+
* `response.cookies[name] = value`.
642+
*/
643+
class AiohttpResponseCookieSubscriptWrite extends HTTP::Server::CookieWrite::Range {
644+
DataFlow::Node index;
645+
DataFlow::Node value;
646+
647+
AiohttpResponseCookieSubscriptWrite() {
648+
exists(Assign assign, Subscript subscript |
649+
// there doesn't seem to be any _good_ choice for `this`, so just picking the
650+
// whole subscript...
651+
this.asExpr() = subscript
652+
|
653+
assign.getATarget() = subscript and
654+
subscript.getObject() = aiohttpResponseInstance().getMember("cookies").getAUse().asExpr() and
655+
index.asExpr() = subscript.getIndex() and
656+
value.asExpr() = assign.getValue()
657+
)
658+
}
659+
660+
override DataFlow::Node getHeaderArg() { none() }
661+
662+
override DataFlow::Node getNameArg() { result = index }
663+
664+
override DataFlow::Node getValueArg() { result = value }
665+
}
562666
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,11 @@ async def redirect_302(request): # $ requestHandler
7272
@routes.get("/setting_cookie") # $ routeSetup="/setting_cookie"
7373
async def setting_cookie(request): # $ requestHandler
7474
resp = web.Response(text="foo") # $ HttpResponse mimetype=text/plain responseBody="foo"
75-
resp.cookies["key"] = "value" # $ MISSING: CookieWrite CookieName="key" CookieValue="value"
75+
resp.cookies["key"] = "value" # $ CookieWrite CookieName="key" CookieValue="value"
7676
resp.headers["Set-Cookie"] = "key2=value2" # $ MISSING: CookieWrite CookieRawHeader="key2=value2"
77-
resp.set_cookie("key3", "value3") # $ MISSING: CookieWrite CookieName="key3" CookieValue="value3"
78-
resp.set_cookie(name="key3", value="value3") # $ MISSING: CookieWrite CookieName="key3" CookieValue="value3"
79-
resp.del_cookie("key4") # $ MISSING: CookieWrite CookieName="key4"
77+
resp.set_cookie("key3", "value3") # $ CookieWrite CookieName="key3" CookieValue="value3"
78+
resp.set_cookie(name="key3", value="value3") # $ CookieWrite CookieName="key3" CookieValue="value3"
79+
resp.del_cookie("key4") # $ CookieWrite CookieName="key4"
8080
return resp
8181

8282

0 commit comments

Comments
 (0)