Skip to content

Commit 62cb4ec

Browse files
authored
Merge pull request #4605 from RasmusWL/python-fix-django-response-modeling
Python: fix django response modeling
2 parents 180373c + 5cf8285 commit 62cb4ec

File tree

2 files changed

+48
-43
lines changed

2 files changed

+48
-43
lines changed

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

Lines changed: 35 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -629,8 +629,7 @@ private module Django {
629629
t.start() and
630630
result = response_attr("HttpResponse")
631631
or
632-
// TODO: remove/expand this part of the template as needed
633-
// Handle `http.HttpResponse` alias
632+
// Handle `django.http.HttpResponse` alias
634633
t.start() and
635634
result = http_attr("HttpResponse")
636635
or
@@ -670,7 +669,7 @@ private module Django {
670669
result.asCfgNode() in [node.getArg(1), node.getArgByName("content_type")]
671670
}
672671

673-
override string getMimetypeDefault() { result = "text/html; charset=utf-8" }
672+
override string getMimetypeDefault() { result = "text/html" }
674673
}
675674

676675
/** Gets a reference to an instance of `django.http.response.HttpResponse`. */
@@ -700,8 +699,7 @@ private module Django {
700699
t.start() and
701700
result = response_attr("HttpResponseRedirect")
702701
or
703-
// TODO: remove/expand this part of the template as needed
704-
// Handle `http.HttpResponseRedirect` alias
702+
// Handle `django.http.HttpResponseRedirect` alias
705703
t.start() and
706704
result = http_attr("HttpResponseRedirect")
707705
or
@@ -732,13 +730,16 @@ private module Django {
732730
ClassInstantiation() { node.getFunction() = classRef().asCfgNode() }
733731

734732
override DataFlow::Node getBody() {
735-
result.asCfgNode() in [node.getArg(0), node.getArgByName("redirect_to")]
733+
// note that even though browsers like Chrome usually doesn't fetch the
734+
// content of a redirect, it is possible to observe the body (for example,
735+
// with cURL).
736+
result.asCfgNode() in [node.getArg(1), node.getArgByName("content")]
736737
}
737738

738739
// How to support the `headers` argument here?
739740
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
740741

741-
override string getMimetypeDefault() { result = "text/html; charset=utf-8" }
742+
override string getMimetypeDefault() { result = "text/html" }
742743
}
743744

744745
/** Gets a reference to an instance of `django.http.response.HttpResponseRedirect`. */
@@ -764,8 +765,7 @@ private module Django {
764765
t.start() and
765766
result = response_attr("HttpResponsePermanentRedirect")
766767
or
767-
// TODO: remove/expand this part of the template as needed
768-
// Handle `http.HttpResponsePermanentRedirect` alias
768+
// Handle `django.http.HttpResponsePermanentRedirect` alias
769769
t.start() and
770770
result = http_attr("HttpResponsePermanentRedirect")
771771
or
@@ -796,13 +796,16 @@ private module Django {
796796
ClassInstantiation() { node.getFunction() = classRef().asCfgNode() }
797797

798798
override DataFlow::Node getBody() {
799-
result.asCfgNode() in [node.getArg(0), node.getArgByName("redirect_to")]
799+
// note that even though browsers like Chrome usually doesn't fetch the
800+
// content of a redirect, it is possible to observe the body (for example,
801+
// with cURL).
802+
result.asCfgNode() in [node.getArg(1), node.getArgByName("content")]
800803
}
801804

802805
// How to support the `headers` argument here?
803806
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
804807

805-
override string getMimetypeDefault() { result = "text/html; charset=utf-8" }
808+
override string getMimetypeDefault() { result = "text/html" }
806809
}
807810

808811
/** Gets a reference to an instance of `django.http.response.HttpResponsePermanentRedirect`. */
@@ -829,7 +832,7 @@ private module Django {
829832
result = response_attr("HttpResponseNotModified")
830833
or
831834
// TODO: remove/expand this part of the template as needed
832-
// Handle `http.HttpResponseNotModified` alias
835+
// Handle `django.http.HttpResponseNotModified` alias
833836
t.start() and
834837
result = http_attr("HttpResponseNotModified")
835838
or
@@ -890,8 +893,7 @@ private module Django {
890893
t.start() and
891894
result = response_attr("HttpResponseBadRequest")
892895
or
893-
// TODO: remove/expand this part of the template as needed
894-
// Handle `http.HttpResponseBadRequest` alias
896+
// Handle `django.http.HttpResponseBadRequest` alias
895897
t.start() and
896898
result = http_attr("HttpResponseBadRequest")
897899
or
@@ -928,7 +930,7 @@ private module Django {
928930
// How to support the `headers` argument here?
929931
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
930932

931-
override string getMimetypeDefault() { result = "text/html; charset=utf-8" }
933+
override string getMimetypeDefault() { result = "text/html" }
932934
}
933935

934936
/** Gets a reference to an instance of `django.http.response.HttpResponseBadRequest`. */
@@ -954,8 +956,7 @@ private module Django {
954956
t.start() and
955957
result = response_attr("HttpResponseNotFound")
956958
or
957-
// TODO: remove/expand this part of the template as needed
958-
// Handle `http.HttpResponseNotFound` alias
959+
// Handle `django.http.HttpResponseNotFound` alias
959960
t.start() and
960961
result = http_attr("HttpResponseNotFound")
961962
or
@@ -992,7 +993,7 @@ private module Django {
992993
// How to support the `headers` argument here?
993994
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
994995

995-
override string getMimetypeDefault() { result = "text/html; charset=utf-8" }
996+
override string getMimetypeDefault() { result = "text/html" }
996997
}
997998

998999
/** Gets a reference to an instance of `django.http.response.HttpResponseNotFound`. */
@@ -1018,8 +1019,7 @@ private module Django {
10181019
t.start() and
10191020
result = response_attr("HttpResponseForbidden")
10201021
or
1021-
// TODO: remove/expand this part of the template as needed
1022-
// Handle `http.HttpResponseForbidden` alias
1022+
// Handle `django.http.HttpResponseForbidden` alias
10231023
t.start() and
10241024
result = http_attr("HttpResponseForbidden")
10251025
or
@@ -1056,7 +1056,7 @@ private module Django {
10561056
// How to support the `headers` argument here?
10571057
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
10581058

1059-
override string getMimetypeDefault() { result = "text/html; charset=utf-8" }
1059+
override string getMimetypeDefault() { result = "text/html" }
10601060
}
10611061

10621062
/** Gets a reference to an instance of `django.http.response.HttpResponseForbidden`. */
@@ -1082,8 +1082,7 @@ private module Django {
10821082
t.start() and
10831083
result = response_attr("HttpResponseNotAllowed")
10841084
or
1085-
// TODO: remove/expand this part of the template as needed
1086-
// Handle `http.HttpResponseNotAllowed` alias
1085+
// Handle `django.http.HttpResponseNotAllowed` alias
10871086
t.start() and
10881087
result = http_attr("HttpResponseNotAllowed")
10891088
or
@@ -1121,7 +1120,7 @@ private module Django {
11211120
// How to support the `headers` argument here?
11221121
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
11231122

1124-
override string getMimetypeDefault() { result = "text/html; charset=utf-8" }
1123+
override string getMimetypeDefault() { result = "text/html" }
11251124
}
11261125

11271126
/** Gets a reference to an instance of `django.http.response.HttpResponseNotAllowed`. */
@@ -1147,8 +1146,7 @@ private module Django {
11471146
t.start() and
11481147
result = response_attr("HttpResponseGone")
11491148
or
1150-
// TODO: remove/expand this part of the template as needed
1151-
// Handle `http.HttpResponseGone` alias
1149+
// Handle `django.http.HttpResponseGone` alias
11521150
t.start() and
11531151
result = http_attr("HttpResponseGone")
11541152
or
@@ -1185,7 +1183,7 @@ private module Django {
11851183
// How to support the `headers` argument here?
11861184
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
11871185

1188-
override string getMimetypeDefault() { result = "text/html; charset=utf-8" }
1186+
override string getMimetypeDefault() { result = "text/html" }
11891187
}
11901188

11911189
/** Gets a reference to an instance of `django.http.response.HttpResponseGone`. */
@@ -1211,8 +1209,7 @@ private module Django {
12111209
t.start() and
12121210
result = response_attr("HttpResponseServerError")
12131211
or
1214-
// TODO: remove/expand this part of the template as needed
1215-
// Handle `http.HttpResponseServerError` alias
1212+
// Handle `django.http.HttpResponseServerError` alias
12161213
t.start() and
12171214
result = http_attr("HttpResponseServerError")
12181215
or
@@ -1249,7 +1246,7 @@ private module Django {
12491246
// How to support the `headers` argument here?
12501247
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
12511248

1252-
override string getMimetypeDefault() { result = "text/html; charset=utf-8" }
1249+
override string getMimetypeDefault() { result = "text/html" }
12531250
}
12541251

12551252
/** Gets a reference to an instance of `django.http.response.HttpResponseServerError`. */
@@ -1275,8 +1272,7 @@ private module Django {
12751272
t.start() and
12761273
result = response_attr("JsonResponse")
12771274
or
1278-
// TODO: remove/expand this part of the template as needed
1279-
// Handle `http.JsonResponse` alias
1275+
// Handle `django.http.JsonResponse` alias
12801276
t.start() and
12811277
result = http_attr("JsonResponse")
12821278
or
@@ -1342,8 +1338,7 @@ private module Django {
13421338
t.start() and
13431339
result = response_attr("StreamingHttpResponse")
13441340
or
1345-
// TODO: remove/expand this part of the template as needed
1346-
// Handle `http.StreamingHttpResponse` alias
1341+
// Handle `django.http.StreamingHttpResponse` alias
13471342
t.start() and
13481343
result = http_attr("StreamingHttpResponse")
13491344
or
@@ -1380,7 +1375,7 @@ private module Django {
13801375
// How to support the `headers` argument here?
13811376
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
13821377

1383-
override string getMimetypeDefault() { result = "text/html; charset=utf-8" }
1378+
override string getMimetypeDefault() { result = "text/html" }
13841379
}
13851380

13861381
/** Gets a reference to an instance of `django.http.response.StreamingHttpResponse`. */
@@ -1406,8 +1401,7 @@ private module Django {
14061401
t.start() and
14071402
result = response_attr("FileResponse")
14081403
or
1409-
// TODO: remove/expand this part of the template as needed
1410-
// Handle `http.FileResponse` alias
1404+
// Handle `django.http.FileResponse` alias
14111405
t.start() and
14121406
result = http_attr("FileResponse")
14131407
or
@@ -1444,7 +1438,10 @@ private module Django {
14441438
// How to support the `headers` argument here?
14451439
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
14461440

1447-
override string getMimetypeDefault() { result = "text/html; charset=utf-8" }
1441+
override string getMimetypeDefault() {
1442+
// see https://github.com/django/django/blob/ebb08d19424c314c75908bc6048ff57c2f872269/django/http/response.py#L471-L479
1443+
result = "application/octet-stream"
1444+
}
14481445
}
14491446

14501447
/** Gets a reference to an instance of `django.http.response.FileResponse`. */

python/ql/test/experimental/library-tests/frameworks/django-v1/response_test.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,27 @@ def safe__manual_content_type(request):
1818
# XSS FP reported in https://github.com/github/codeql/issues/3466
1919
# Note: This should be an open-redirect sink, but not an XSS sink.
2020
def or__redirect(request):
21-
return HttpResponseRedirect(request.GET.get("next")) # $HttpResponse mimetype="text/html; charset=utf-8" responseBody=Attribute()
21+
return HttpResponseRedirect(request.GET.get("next")) # $HttpResponse mimetype=text/html
22+
23+
def information_exposure_through_redirect(request, as_kw=False):
24+
# This is a contrived example, but possible
25+
private = "private"
26+
if as_kw:
27+
return HttpResponseRedirect(request.GET.get("next"), content=private) # $HttpResponse mimetype=text/html responseBody=private
28+
else:
29+
return HttpResponseRedirect(request.GET.get("next"), private) # $HttpResponse mimetype=text/html responseBody=private
2230

2331
# Ensure that simple subclasses are still vuln to XSS
2432
def xss__not_found(request):
25-
return HttpResponseNotFound(request.GET.get("name")) # $HttpResponse mimetype="text/html; charset=utf-8" responseBody=Attribute()
33+
return HttpResponseNotFound(request.GET.get("name")) # $HttpResponse mimetype=text/html responseBody=Attribute()
2634

2735
# Ensure we still have an XSS sink when manually setting the content_type to HTML
2836
def xss__manual_response_type(request):
2937
return HttpResponse(request.GET.get("name"), content_type="text/html; charset=utf-8") # $HttpResponse mimetype=text/html responseBody=Attribute()
3038

3139
def xss__write(request):
32-
response = HttpResponse() # $HttpResponse mimetype="text/html; charset=utf-8"
33-
response.write(request.GET.get("name")) # $HttpResponse mimetype="text/html; charset=utf-8" responseBody=Attribute()
40+
response = HttpResponse() # $HttpResponse mimetype=text/html
41+
response.write(request.GET.get("name")) # $HttpResponse mimetype=text/html responseBody=Attribute()
3442

3543
# This is safe but probably a bug if the argument to `write` is not a result of `json.dumps` or similar.
3644
def safe__write_json(request):
@@ -50,4 +58,4 @@ def __init__(self, banner, content, *args, **kwargs):
5058
super().__init__(content, *args, content_type="text/html", **kwargs)
5159

5260
def safe__custom_json_response(request):
53-
return CustomJsonResponse("ACME Responses", {"foo": request.GET.get("foo")}) # $HttpResponse mimetype=application/json MISSING: responseBody=Dict SPURIOUS: responseBody="ACME Responses"
61+
return CustomJsonResponse("ACME Responses", {"foo": request.GET.get("foo")}) # $HttpResponse mimetype=application/json MISSING: responseBody=Dict SPURIOUS: responseBody="ACME Responses"

0 commit comments

Comments
 (0)