Skip to content

Commit e1af1f1

Browse files
committed
Python: Add HTTP::Server::CookieWrite concept
along with tests, but no implementations (to ease reviewing). --- I've put quite some thinking into what to call our concept for this. [JS has `CookieDefinition`](https://github.com/github/codeql/blob/581f4ed757eeebd1de472e30be9e03e87904b837/javascript/ql/src/semmle/javascript/frameworks/HTTP.qll#L148-L187), but I couldn't find a matching concept in any other languages. We used to call this [`CookieSet`](https://github.com/github/codeql/blob/f07a7bf8cff4152845a013fa44001247e796b3a0/python/ql/src/semmle/python/web/Http.qll#L76) (and had a corresponding `CookieGet`). But for headers, [Go calls this `HeaderWrite`](https://github.com/github/codeql-go/blob/cd1e14ed09f4b56229b5c4fb7797203193b93897/ql/src/semmle/go/concepts/HTTP.qll#L97-L131) and [JS calls this `HeaderDefinition`](https://github.com/github/codeql/blob/581f4ed757eeebd1de472e30be9e03e87904b837/javascript/ql/src/semmle/javascript/frameworks/HTTP.qll#L23-L46) I think it would be really cool if we have a naming scheme that means the name for getting the value of a header on a incoming request is obvious. I think `HeaderWrite`/`HeaderRead` fulfils this best. We could go with `HeaderSet`/`HeaderGet`, but they feel a bit too vague to me. For me, I'm so used to talking about def-use, that I would immediately go for `HeaderDefinition` and `HeaderUse`, which could work, but is kinda strange. So in the end that means I went with `CookieWrite`, since that allows using a consistent naming scheme for the future :)
1 parent 902b450 commit e1af1f1

File tree

7 files changed

+151
-6
lines changed

7 files changed

+151
-6
lines changed

python/ql/src/semmle/python/Concepts.qll

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,62 @@ module HTTP {
555555
abstract DataFlow::Node getRedirectLocation();
556556
}
557557
}
558+
559+
/**
560+
* A data-flow node that sets a cookie in an HTTP response.
561+
*
562+
* Extend this class to refine existing API models. If you want to model new APIs,
563+
* extend `HTTP::CookieWrite::Range` instead.
564+
*/
565+
class CookieWrite extends DataFlow::Node {
566+
CookieWrite::Range range;
567+
568+
CookieWrite() { this = range }
569+
570+
/**
571+
* Gets the argument, if any, specifying the raw cookie header.
572+
*/
573+
DataFlow::Node getHeaderArg() { result = range.getHeaderArg() }
574+
575+
/**
576+
* Gets the argument, if any, specifying the cookie name.
577+
*/
578+
DataFlow::Node getNameArg() { result = range.getNameArg() }
579+
580+
/**
581+
* Gets the argument, if any, specifying the cookie value.
582+
*/
583+
DataFlow::Node getValueArg() { result = range.getValueArg() }
584+
}
585+
586+
/** Provides a class for modeling new cookie writes on HTTP responses. */
587+
module CookieWrite {
588+
/**
589+
* A data-flow node that sets a cookie in an HTTP response.
590+
*
591+
* Note: we don't require that this redirect must be sent to a client (a kind of
592+
* "if a tree falls in a forest and nobody hears it" situation).
593+
*
594+
* Extend this class to model new APIs. If you want to refine existing API models,
595+
* extend `HttpResponse` instead.
596+
*/
597+
abstract class Range extends DataFlow::Node {
598+
/**
599+
* Gets the argument, if any, specifying the raw cookie header.
600+
*/
601+
abstract DataFlow::Node getHeaderArg();
602+
603+
/**
604+
* Gets the argument, if any, specifying the cookie name.
605+
*/
606+
abstract DataFlow::Node getNameArg();
607+
608+
/**
609+
* Gets the argument, if any, specifying the cookie value.
610+
*/
611+
abstract DataFlow::Node getValueArg();
612+
}
613+
}
558614
}
559615
}
560616

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,38 @@ class HttpServerHttpRedirectResponseTest extends InlineExpectationsTest {
252252
}
253253
}
254254

255+
class HttpServerCookieWriteTest extends InlineExpectationsTest {
256+
HttpServerCookieWriteTest() { this = "HttpServerCookieWriteTest" }
257+
258+
override string getARelevantTag() {
259+
result in ["CookieWrite", "CookieRawHeader", "CookieName", "CookieValue"]
260+
}
261+
262+
override predicate hasActualResult(Location location, string element, string tag, string value) {
263+
exists(location.getFile().getRelativePath()) and
264+
exists(HTTP::Server::CookieWrite cookieWrite |
265+
location = cookieWrite.getLocation() and
266+
(
267+
element = cookieWrite.toString() and
268+
value = "" and
269+
tag = "CookieWrite"
270+
or
271+
element = cookieWrite.toString() and
272+
value = prettyNodeForInlineTest(cookieWrite.getHeaderArg()) and
273+
tag = "CookieRawHeader"
274+
or
275+
element = cookieWrite.toString() and
276+
value = prettyNodeForInlineTest(cookieWrite.getNameArg()) and
277+
tag = "CookieName"
278+
or
279+
element = cookieWrite.toString() and
280+
value = prettyNodeForInlineTest(cookieWrite.getValueArg()) and
281+
tag = "CookieValue"
282+
)
283+
)
284+
}
285+
}
286+
255287
class FileSystemAccessTest extends InlineExpectationsTest {
256288
FileSystemAccessTest() { this = "FileSystemAccessTest" }
257289

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,20 @@ async def redirect_302(request): # $ requestHandler
6565
else:
6666
raise web.HTTPFound(location="/logout") # $ HttpResponse HttpRedirectResponse mimetype=application/octet-stream redirectLocation="/logout"
6767

68+
################################################################################
69+
# Cookies
70+
################################################################################
71+
72+
@routes.get("/setting_cookie") # $ routeSetup="/setting_cookie"
73+
async def setting_cookie(request): # $ requestHandler
74+
resp = web.Response(text="foo") # $ HttpResponse mimetype=text/plain responseBody="foo"
75+
resp.cookies["key"] = "value" # $ MISSING: CookieWrite CookieName="key" CookieValue="value"
76+
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"
80+
return resp
81+
6882

6983
if __name__ == "__main__":
7084
app = web.Application()

python/ql/test/library-tests/frameworks/django-v2-v3/response_test.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,15 @@ def __init__(self, banner, content, *args, **kwargs):
103103

104104
def safe__custom_json_response(request):
105105
return CustomJsonResponse("ACME Responses", {"foo": request.GET.get("foo")}) # $HttpResponse mimetype=application/json MISSING: responseBody=Dict SPURIOUS: responseBody="ACME Responses"
106+
107+
################################################################################
108+
# Cookies
109+
################################################################################
110+
111+
def setting_cookie(request):
112+
resp = HttpResponse() # $ HttpResponse mimetype=text/html
113+
resp.set_cookie("key", "value") # $ MISSING: CookieWrite CookieName="key" CookieValue="value"
114+
resp.set_cookie(key="key", value="value") # $ MISSING: CookieWrite CookieName="key" CookieValue="value"
115+
resp.headers["Set-Cookie"] = "key2=value2" # $ MISSING: CookieWrite CookieRawHeader="key2=value2"
116+
resp.cookies["key3"] = "value3" # $ MISSING: CookieWrite CookieName="key3" CookieValue="value3"
117+
return resp

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,19 @@ def redirect_simple(): # $requestHandler
184184
return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp
185185

186186

187+
################################################################################
188+
# Cookies
189+
################################################################################
190+
191+
@app.route("/setting_cookie") # $routeSetup="/setting_cookie"
192+
def setting_cookie(): # $requestHandler
193+
resp = make_response() # $ HttpResponse mimetype=text/html
194+
resp.set_cookie("key", "value") # $ MISSING: CookieWrite CookieName="key" CookieValue="value"
195+
resp.set_cookie(key="key", value="value") # $ MISSING: CookieWrite CookieName="key" CookieValue="value"
196+
resp.headers.add("Set-Cookie", "key2=value2") # $ MISSING: CookieWrite CookieRawHeader="key2=value2"
197+
resp.delete_cookie("key3") # $ MISSING: CookieWrite CookieName="key3"
198+
return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp
199+
187200
################################################################################
188201

189202

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,18 @@ def get(self, stream=False): # $ requestHandler routedParameter=stream
5858
stream.write(b"foo stream") # $ MISSING: HttpResponse responseBody=b"foo stream"
5959
stream.close()
6060

61+
################################################################################
62+
# Cookies
63+
################################################################################
64+
65+
class CookieWriting(tornado.web.RequestHandler):
66+
def get(self): # $ requestHandler
67+
self.write("foo") # $ HttpResponse mimetype=text/html responseBody="foo"
68+
self.set_cookie("key", "value") # $ MISSING: CookieWrite CookieName="key" CookieValue="value"
69+
self.set_cookie(name="key", value="value") # $ MISSING: CookieWrite CookieName="key" CookieValue="value"
70+
self.set_header("Set-Cookie", "key2=value2") # $ MISSING: CookieWrite CookieRawHeader="key2=value2"
71+
72+
6173
def make_app():
6274
return tornado.web.Application(
6375
[
@@ -66,6 +78,7 @@ def make_app():
6678
(r"/ExampleRedirect", ExampleRedirect), # $ routeSetup="/ExampleRedirect"
6779
(r"/ExampleConnectionWrite", ExampleConnectionWrite), # $ routeSetup="/ExampleConnectionWrite"
6880
(r"/ExampleConnectionWrite/(stream)", ExampleConnectionWrite), # $ routeSetup="/ExampleConnectionWrite/(stream)"
81+
(r"/CookieWriting", CookieWriting), # $ routeSetup="/CookieWriting"
6982
],
7083
debug=True,
7184
)
@@ -74,6 +87,7 @@ def make_app():
7487
if __name__ == "__main__":
7588
import tornado.ioloop
7689

90+
print("running on http://localhost:8888/")
7791
app = make_app()
7892
app.listen(8888)
7993
tornado.ioloop.IOLoop.current().start()

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,20 @@ def render_GET(self, request: Request): # $ requestHandler
4343
# requested with curl.
4444
return b"hello" # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=b"hello"
4545

46+
################################################################################
47+
# Cookies
48+
################################################################################
4649

47-
class NonHttpBodyOutput(Resource):
50+
class CookieWriting(Resource):
4851
"""Examples of providing values in response that is not in the body
4952
"""
5053
def render_GET(self, request: Request): # $ requestHandler
51-
request.responseHeaders.addRawHeader("key", "value")
52-
request.setHeader("key2", "value")
54+
request.addCookie("key", "value") # $ MISSING: CookieWrite CookieName="key" CookieValue="value"
55+
request.addCookie(k="key", v="value") # $ MISSING: CookieWrite CookieName="key" CookieValue="value"
56+
request.cookies.append("key2=value") # $ MISSING: CookieWrite CookieRawHeader="key2=value2"
5357

54-
request.addCookie("key", "value")
55-
request.cookies.append(b"key2=value")
58+
request.responseHeaders.addRawHeader("key", "value")
59+
request.setHeader("Set-Cookie", "key3=value3") # $ MISSING: CookieWrite CookieRawHeader="key3=value3"
5660

5761
return b"" # $ HttpResponse mimetype=text/html responseBody=b""
5862

@@ -62,7 +66,7 @@ def render_GET(self, request: Request): # $ requestHandler
6266
root.putChild(b"later", Later())
6367
root.putChild(b"plain-text", PlainText())
6468
root.putChild(b"redirect", Redirect())
65-
root.putChild(b"non-body", NonHttpBodyOutput())
69+
root.putChild(b"setting_cookie", CookieWriting())
6670

6771

6872
if __name__ == "__main__":

0 commit comments

Comments
 (0)