Skip to content

Commit 2b935e5

Browse files
Add concept tests + fix typo
1 parent ec4c820 commit 2b935e5

File tree

5 files changed

+94
-12
lines changed

5 files changed

+94
-12
lines changed

python/ql/lib/semmle/python/Concepts.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1094,7 +1094,7 @@ module Http {
10941094
/**
10951095
* Gets the argument containing the headers dictionary.
10961096
*/
1097-
DataFlow::Node geBulkArg() { result = super.getBulkArg() }
1097+
DataFlow::Node getBulkArg() { result = super.getBulkArg() }
10981098

10991099
/**
11001100
* Holds if newlines are accepted in the header name argument.

python/ql/lib/semmle/python/security/dataflow/HttpHeaderInjectionCustomizations.qll

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ module HttpHeaderInjection {
5858
KeyValuePair item;
5959

6060
HeaderBulkWriteDictLiteral() {
61-
exists(Dict dict | DataFlow::localFlow(DataFlow::exprNode(dict), super.geBulkArg()) |
61+
exists(Dict dict | DataFlow::localFlow(DataFlow::exprNode(dict), super.getBulkArg()) |
6262
item = dict.getAnItem()
6363
)
6464
}
@@ -83,7 +83,7 @@ module HttpHeaderInjection {
8383
Tuple item;
8484

8585
HeaderBulkWriteListLiteral() {
86-
exists(List list | DataFlow::localFlow(DataFlow::exprNode(list), super.geBulkArg()) |
86+
exists(List list | DataFlow::localFlow(DataFlow::exprNode(list), super.getBulkArg()) |
8787
item = list.getAnElt()
8888
)
8989
}

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

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,43 @@ module HttpServerHttpResponseTest implements TestSig {
319319
}
320320
}
321321

322+
module HttpResponseHeaderWriteTest implements TestSig {
323+
string getARelevantTag() { result = ["headerWriteName", "headerWriteValue", "headerWriteBulk"] }
324+
325+
predicate hasActualResult(Location location, string element, string tag, string value) {
326+
exists(location.getFile().getRelativePath()) and
327+
(
328+
exists(Http::Server::ResponseHeaderWrite write, DataFlow::Node node |
329+
location = node.getLocation() and
330+
element = node.toString()
331+
|
332+
node = write.getNameArg() and
333+
tag = "headerWriteName" and
334+
(if write.nameAllowsNewline() then value = "unsanitized" else value = "sanitized")
335+
or
336+
node = write.getValueArg() and
337+
tag = "headerWriteValue" and
338+
(if write.valueAllowsNewline() then value = "unsanitized" else value = "sanitized")
339+
)
340+
or
341+
exists(Http::Server::ResponseHeaderBulkWrite write |
342+
location = write.getBulkArg().getLocation() and
343+
element = write.getBulkArg().toString() and
344+
(
345+
tag = "headerWriteBulk" and
346+
value = ""
347+
or
348+
tag = "headerWriteName" and
349+
(if write.nameAllowsNewline() then value = "unsanitized" else value = "sanitized")
350+
or
351+
tag = "headerWriteValue" and
352+
(if write.valueAllowsNewline() then value = "unsanitized" else value = "sanitized")
353+
)
354+
)
355+
)
356+
}
357+
}
358+
322359
module HttpServerHttpRedirectResponseTest implements TestSig {
323360
string getARelevantTag() { result in ["HttpRedirectResponse", "redirectLocation"] }
324361

@@ -559,7 +596,8 @@ import MakeTest<MergeTests5<MergeTests5<SystemCommandExecutionTest, DecodingTest
559596
MergeTests5<SqlConstructionTest, SqlExecutionTest, XPathConstructionTest, XPathExecutionTest,
560597
EscapingTest>,
561598
MergeTests5<HttpServerRouteSetupTest, HttpServerRequestHandlerTest, HttpServerHttpResponseTest,
562-
HttpServerHttpRedirectResponseTest, HttpServerCookieWriteTest>,
599+
HttpServerHttpRedirectResponseTest,
600+
MergeTests<HttpServerCookieWriteTest, HttpResponseHeaderWriteTest>>,
563601
MergeTests5<FileSystemAccessTest, FileSystemWriteAccessTest, PathNormalizationTest,
564602
SafeAccessCheckTest, PublicKeyGenerationTest>,
565603
MergeTests5<CryptographicOperationTest, HttpClientRequestTest, CsrfProtectionSettingTest,

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

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22

33
from flask import Flask, make_response, jsonify, Response, request, redirect
4+
from werkzeug.datastructures import Headers
45

56
app = Flask(__name__)
67

@@ -117,7 +118,7 @@ def response_modification1(): # $requestHandler
117118
@app.route("/content-type/response-modification2") # $routeSetup="/content-type/response-modification2"
118119
def response_modification2(): # $requestHandler
119120
resp = make_response("<h1>hello</h1>") # $HttpResponse mimetype=text/html responseBody="<h1>hello</h1>"
120-
resp.headers["content-type"] = "text/plain" # $ MISSING: HttpResponse mimetype=text/plain
121+
resp.headers["content-type"] = "text/plain" # $ headerWriteName=unsanitized headerWriteValue=sanitized MISSING: HttpResponse mimetype=text/plain
121122
return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp
122123

123124

@@ -147,23 +148,23 @@ def Response3(): # $requestHandler
147148
@app.route("/content-type/Response4") # $routeSetup="/content-type/Response4"
148149
def Response4(): # $requestHandler
149150
# note: capitalization of Content-Type does not matter
150-
resp = Response("<h1>hello</h1>", headers={"Content-TYPE": "text/plain"}) # $HttpResponse responseBody="<h1>hello</h1>" SPURIOUS: mimetype=text/html MISSING: mimetype=text/plain
151+
resp = Response("<h1>hello</h1>", headers={"Content-TYPE": "text/plain"}) # $ headerWriteBulk headerWriteName=unsanitized headerWriteValue=sanitized HttpResponse responseBody="<h1>hello</h1>" SPURIOUS: mimetype=text/html MISSING: mimetype=text/plain
151152
return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp
152153

153154

154155
@app.route("/content-type/Response5") # $routeSetup="/content-type/Response5"
155156
def Response5(): # $requestHandler
156157
# content_type argument takes priority (and result is text/plain)
157158
# note: capitalization of Content-Type does not matter
158-
resp = Response("<h1>hello</h1>", headers={"Content-TYPE": "text/html"}, content_type="text/plain; charset=utf-8") # $HttpResponse mimetype=text/plain responseBody="<h1>hello</h1>"
159+
resp = Response("<h1>hello</h1>", headers={"Content-TYPE": "text/html"}, content_type="text/plain; charset=utf-8") # $ headerWriteBulk headerWriteName=unsanitized headerWriteValue=sanitized HttpResponse mimetype=text/plain responseBody="<h1>hello</h1>"
159160
return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp
160161

161162

162163
@app.route("/content-type/Response6") # $routeSetup="/content-type/Response6"
163164
def Response6(): # $requestHandler
164165
# mimetype argument takes priority over header (and result is text/plain)
165166
# note: capitalization of Content-Type does not matter
166-
resp = Response("<h1>hello</h1>", headers={"Content-TYPE": "text/html"}, mimetype="text/plain") # $HttpResponse mimetype=text/plain responseBody="<h1>hello</h1>"
167+
resp = Response("<h1>hello</h1>", headers={"Content-TYPE": "text/html"}, mimetype="text/plain") # $ headerWriteBulk headerWriteName=unsanitized headerWriteValue=sanitized HttpResponse mimetype=text/plain responseBody="<h1>hello</h1>"
167168
return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp
168169

169170

@@ -207,12 +208,44 @@ def setting_cookie(): # $requestHandler
207208
resp = make_response() # $ HttpResponse mimetype=text/html
208209
resp.set_cookie("key", "value") # $ CookieWrite CookieName="key" CookieValue="value"
209210
resp.set_cookie(key="key", value="value") # $ CookieWrite CookieName="key" CookieValue="value"
210-
resp.headers.add("Set-Cookie", "key2=value2") # $ MISSING: CookieWrite CookieRawHeader="key2=value2"
211+
resp.headers.add("Set-Cookie", "key2=value2") # $ headerWriteName=unsanitized headerWriteValue=sanitized MISSING: CookieWrite CookieRawHeader="key2=value2"
211212
resp.delete_cookie("key3") # $ CookieWrite CookieName="key3"
212213
resp.delete_cookie(key="key3") # $ CookieWrite CookieName="key3"
213214
return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp
214215

215216
################################################################################
217+
# Headers
218+
################################################################################
219+
220+
@app.route("/headers") # $routeSetup="/headers"
221+
def headers(): # $requestHandler
222+
resp1 = Response() # $ HttpResponse mimetype=text/html
223+
resp1.headers['X-MyHeader'] = 'a' # $ headerWriteName=unsanitized headerWriteValue=sanitized
224+
resp2 = make_response() # $ HttpResponse mimetype=text/html
225+
resp2.headers['X-MyHeader'] = 'a' # $ headerWriteName=unsanitized headerWriteValue=sanitized
226+
resp2.headers.extend({'X-MyHeader2': 'b'}) # $ headerWriteBulk headerWriteName=unsanitized headerWriteValue=sanitized
227+
resp3 = make_response("hello", 200, {'X-MyHeader3': 'c'}) # $ HttpResponse mimetype=text/html responseBody="hello" headerWriteBulk headerWriteName=unsanitized headerWriteValue=sanitized
228+
resp4 = make_response("hello", {'X-MyHeader4': 'd'}) # $ HttpResponse mimetype=text/html responseBody="hello" headerWriteBulk headerWriteName=unsanitized headerWriteValue=sanitized
229+
return resp4 # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp4
230+
231+
@app.route("/werkzeug-headers") # $routeSetup="/werkzeug-headers"
232+
def werkzeug_headers(): # $requestHandler
233+
response = Response() # $ HttpResponse mimetype=text/html
234+
headers = Headers()
235+
headers.add("X-MyHeader1", "a") # $ headerWriteName=unsanitized headerWriteValue=sanitized
236+
headers.add_header("X-MyHeader2", "b") # $ headerWriteName=unsanitized headerWriteValue=sanitized
237+
headers.set("X-MyHeader3", "c") # $ headerWriteName=unsanitized headerWriteValue=sanitized
238+
headers.setdefault("X-MyHeader4", "d") # $ headerWriteName=unsanitized headerWriteValue=sanitized
239+
headers.__setitem__("X-MyHeader5", "e") # $ headerWriteName=unsanitized headerWriteValue=sanitized
240+
headers["X-MyHeader6"] = "f" # $ headerWriteName=unsanitized headerWriteValue=sanitized
241+
h1 = {"X-MyHeader7": "g"}
242+
headers.extend(h1) # $ headerWriteBulk headerWriteName=unsanitized headerWriteValue=sanitized
243+
h2 = [("X-MyHeader8", "h")]
244+
headers.extend(h2) # $ headerWriteBulk headerWriteName=unsanitized headerWriteValue=sanitized
245+
response.headers = headers
246+
return response # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=response
247+
248+
################################################################################
216249

217250

218251
if __name__ == "__main__":

python/ql/test/library-tests/frameworks/stdlib/wsgiref_simple_server_test.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
# see https://docs.python.org/3/library/wsgiref.html#wsgiref.simple_server.WSGIServer
33
import sys
44
import wsgiref.simple_server
5+
import wsgiref.headers
56

67
def ignore(*arg, **kwargs): pass
78
ensure_tainted = ensure_not_tainted = ignore
@@ -17,7 +18,7 @@ def func(environ, start_response): # $ requestHandler
1718
environ, # $ tainted
1819
environ["PATH_INFO"], # $ tainted
1920
)
20-
write = start_response("200 OK", [("Content-Type", "text/plain")])
21+
write = start_response("200 OK", [("Content-Type", "text/plain")]) # $ headerWriteBulk headerWriteName=unsanitized headerWriteValue=unsanitized
2122
write(b"hello") # $ HttpResponse responseBody=b"hello"
2223
write(data=b" ") # $ HttpResponse responseBody=b" "
2324

@@ -32,9 +33,17 @@ def __init__(self):
3233
self.set_app(self.my_method)
3334

3435
def my_method(self, _env, start_response): # $ requestHandler
35-
start_response("200 OK", [])
36+
start_response("200 OK", []) # $ headerWriteBulk headerWriteName=unsanitized headerWriteValue=unsanitized
3637
return [b"my_method"] # $ HttpResponse responseBody=List
3738

39+
def func2(environ, start_response): # $ requestHandler
40+
headers = wsgiref.headers.Headers([("Content-Type", "text/plain")]) # $ headerWriteBulk headerWriteName=unsanitized headerWriteValue=unsanitized
41+
headers.add_header("X-MyHeader", "a") # $ headerWriteName=unsanitized headerWriteValue=unsanitized
42+
headers.setdefault("X-MyHeader2", "b") # $ headerWriteName=unsanitized headerWriteValue=unsanitized
43+
headers.__setitem__("X-MyHeader3", "c") # $ headerWriteName=unsanitized headerWriteValue=unsanitized
44+
headers["X-MyHeader4"] = "d" # $ headerWriteName=unsanitized headerWriteValue=unsanitized
45+
start_response(status, headers) # $ headerWriteBulk headerWriteName=unsanitized headerWriteValue=unsanitized
46+
return [b"Hello"] # $ HttpResponse responseBody=List
3847

3948
case = sys.argv[1]
4049
if case == "1":
@@ -45,9 +54,11 @@ def my_method(self, _env, start_response): # $ requestHandler
4554
elif case == "3":
4655
server = MyServer()
4756
def func3(_env, start_response): # $ requestHandler
48-
start_response("200 OK", [])
57+
start_response("200 OK", []) # $ headerWriteBulk headerWriteName=unsanitized headerWriteValue=unsanitized
4958
return [b"foo"] # $ HttpResponse responseBody=List
5059
server.set_app(func3)
60+
elif case == "4":
61+
server = wsgiref.simple_server.make_server(ADDRESS[0], ADDRESS[1], func2)
5162
else:
5263
sys.exit("wrong case")
5364

0 commit comments

Comments
 (0)