Skip to content

Commit fa08676

Browse files
committed
Python: Proper redirect taint sinks for Django
Also a major restructuring of the code. A bit controversial since it renames/moves classes that are already public. Fixes github#3466
1 parent 72ea4ff commit fa08676

File tree

6 files changed

+73
-46
lines changed

6 files changed

+73
-46
lines changed

python/ql/src/semmle/python/web/django/Redirect.qll

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,29 @@ private import semmle.python.web.django.Shared
1111
private import semmle.python.web.Http
1212

1313
/**
14-
* Represents an argument to the `django.redirect` function.
14+
* The URL argument for a call to the `django.shortcuts.redirect` function.
1515
*/
16-
class DjangoRedirect extends HttpRedirectTaintSink {
17-
override string toString() { result = "django.redirect" }
16+
class DjangoShortcutsRedirectSink extends HttpRedirectTaintSink {
17+
override string toString() { result = "DjangoShortcutsRedirectSink" }
1818

19-
DjangoRedirect() { this = redirect().getACall().getAnArg() }
19+
DjangoShortcutsRedirectSink() {
20+
this = Value::named("django.shortcuts.redirect").(FunctionValue).getArgumentForCall(_, 0)
21+
}
22+
}
23+
24+
/**
25+
* The URL argument when instantiating a Django Redirect Response.
26+
*/
27+
class DjangoRedirectResponseSink extends HttpRedirectTaintSink {
28+
DjangoRedirectResponseSink() {
29+
exists(CallNode call |
30+
call = any(DjangoRedirectResponse rr).getACall()
31+
|
32+
this = call.getArg(0)
33+
or
34+
this = call.getArgByName("redirect_to")
35+
)
36+
}
37+
38+
override string toString() { result = "DjangoRedirectResponseSink" }
2039
}

python/ql/src/semmle/python/web/django/Response.qll

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4,38 +4,20 @@ import semmle.python.security.strings.Basic
44
private import semmle.python.web.django.Shared
55
private import semmle.python.web.Http
66

7-
/**
8-
* A django.http.response.Response object
9-
* This isn't really a "taint", but we use the value tracking machinery to
10-
* track the flow of response objects.
11-
*/
12-
class DjangoResponse extends TaintKind {
13-
DjangoResponse() { this = "django.response.HttpResponse" }
7+
/** INTERNAL class used for tracking a django response object. */
8+
private class DjangoResponseKind extends TaintKind {
9+
DjangoResponseKind() { this = "django.response.HttpResponse" }
1410
}
1511

16-
private ClassValue theDjangoHttpResponseClass() {
17-
(
18-
// version 1.x
19-
result = Value::named("django.http.response.HttpResponse")
20-
or
21-
// version 2.x
22-
// https://docs.djangoproject.com/en/2.2/ref/request-response/#httpresponse-objects
23-
result = Value::named("django.http.HttpResponse")
24-
) and
25-
// TODO: does this do anything? when could they be the same???
26-
not result = theDjangoHttpRedirectClass()
27-
}
28-
29-
/** internal class used for tracking a django response. */
12+
/** INTENRAL taint-source used for tracking a django response. */
3013
private class DjangoResponseSource extends TaintSource {
3114
DjangoResponseSource() {
32-
exists(ClassValue cls |
33-
cls.getASuperType() = theDjangoHttpResponseClass() and
15+
exists(DjangoXSSVulnResponse cls |
3416
cls.getACall() = this
3517
)
3618
}
3719

38-
override predicate isSourceOf(TaintKind kind) { kind instanceof DjangoResponse }
20+
override predicate isSourceOf(TaintKind kind) { kind instanceof DjangoResponseKind }
3921

4022
override string toString() { result = "django.http.response.HttpResponse" }
4123
}
@@ -45,7 +27,7 @@ class DjangoResponseWrite extends HttpResponseTaintSink {
4527
DjangoResponseWrite() {
4628
exists(AttrNode meth, CallNode call |
4729
call.getFunction() = meth and
48-
any(DjangoResponse response).taints(meth.getObject("write")) and
30+
any(DjangoResponseKind response).taints(meth.getObject("write")) and
4931
this = call.getArg(0)
5032
)
5133
}
@@ -58,9 +40,8 @@ class DjangoResponseWrite extends HttpResponseTaintSink {
5840
/** An argument to initialization of a django response, which is vulnerable to external data (xss) */
5941
class DjangoResponseContent extends HttpResponseTaintSink {
6042
DjangoResponseContent() {
61-
exists(CallNode call, ClassValue cls |
62-
cls.getASuperType() = theDjangoHttpResponseClass() and
63-
call.getFunction().pointsTo(cls)
43+
exists(CallNode call, DjangoXSSVulnResponse cls |
44+
call = cls.getACall()
6445
|
6546
call.getArg(0) = this
6647
or
@@ -75,7 +56,7 @@ class DjangoResponseContent extends HttpResponseTaintSink {
7556

7657
class DjangoCookieSet extends CookieSet, CallNode {
7758
DjangoCookieSet() {
78-
any(DjangoResponse r).taints(this.getFunction().(AttrNode).getObject("set_cookie"))
59+
any(DjangoResponseKind r).taints(this.getFunction().(AttrNode).getObject("set_cookie"))
7960
}
8061

8162
override string toString() { result = CallNode.super.toString() }
Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,39 @@
11
import python
22

3-
/** django.shortcuts.redirect */
4-
FunctionValue redirect() { result = Value::named("django.shortcuts.redirect") }
3+
/** A Class that is a Django Response (subclass of `django.http.HttpResponse`). */
4+
class DjangoResponse extends ClassValue {
5+
DjangoResponse() {
6+
exists(ClassValue base |
7+
// version 1.x
8+
base = Value::named("django.http.response.HttpResponse")
9+
or
10+
// version 2.x and 3.x
11+
// https://docs.djangoproject.com/en/2.2/ref/request-response/#httpresponse-objects
12+
base = Value::named("django.http.HttpResponse")
13+
|
14+
this.getASuperType() = base
15+
)
16+
}
17+
}
18+
19+
/** A Class that is a Django Redirect Response (subclass of `django.http.HttpResponseRedirectBase`). */
20+
class DjangoRedirectResponse extends DjangoResponse {
21+
DjangoRedirectResponse() {
22+
exists(ClassValue base |
23+
// version 1.x
24+
base = Value::named("django.http.response.HttpResponseRedirectBase")
25+
or
26+
// version 2.x and 3.x
27+
base = Value::named("django.http.HttpResponseRedirectBase")
28+
|
29+
this.getASuperType() = base
30+
)
31+
}
32+
}
533

6-
ClassValue theDjangoHttpRedirectClass() {
7-
// version 1.x
8-
result = Value::named("django.http.response.HttpResponseRedirectBase")
9-
or
10-
// version 2.x
11-
result = Value::named("django.http.HttpResponseRedirectBase")
34+
/** A Class that is a Django Response, and is vulnerable to XSS. */
35+
class DjangoXSSVulnResponse extends DjangoResponse {
36+
DjangoXSSVulnResponse() {
37+
not this instanceof DjangoRedirectResponse
38+
}
1239
}
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
1-
| test_1x.py:13:21:13:24 | django.redirect | externally controlled string |
2-
| test_2x_3x.py:13:21:13:24 | django.redirect | externally controlled string |
1+
| test_1x.py:13:21:13:24 | DjangoShortcutsRedirectSink | externally controlled string |
2+
| test_2x_3x.py:13:21:13:24 | DjangoShortcutsRedirectSink | externally controlled string |
3+
| views_1x.py:99:33:99:55 | DjangoRedirectResponseSink | externally controlled string |
4+
| views_2x_3x.py:120:33:120:55 | DjangoRedirectResponseSink | externally controlled string |

python/ql/test/library-tests/web/django/HttpResponseSinks.expected

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
| views_1x.py:85:25:85:55 | django.Response(...) | externally controlled string |
1212
| views_1x.py:90:25:90:33 | django.Response(...) | externally controlled string |
1313
| views_1x.py:94:25:94:58 | django.Response(...) | externally controlled string |
14-
| views_1x.py:99:33:99:55 | django.Response(...) | externally controlled string |
1514
| views_1x.py:103:33:103:55 | django.Response(...) | externally controlled string |
1615
| views_2x_3x.py:8:25:8:63 | django.Response(...) | externally controlled string |
1716
| views_2x_3x.py:12:25:12:52 | django.Response(...) | externally controlled string |
@@ -29,5 +28,4 @@
2928
| views_2x_3x.py:106:25:106:55 | django.Response(...) | externally controlled string |
3029
| views_2x_3x.py:111:25:111:33 | django.Response(...) | externally controlled string |
3130
| views_2x_3x.py:115:25:115:58 | django.Response(...) | externally controlled string |
32-
| views_2x_3x.py:120:33:120:55 | django.Response(...) | externally controlled string |
3331
| views_2x_3x.py:124:33:124:55 | django.Response(...) | externally controlled string |

python/ql/test/library-tests/web/django/views_2x_3x.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def fp_manual_content_type(reuqest):
117117
# XSS FP reported in https://github.com/github/codeql/issues/3466
118118
# Note: This should be a open-redirect sink, but not a XSS sink.
119119
def fp_redirect(request):
120-
return HttpResponseRedirect(request.GET.get("next")) # TODO
120+
return HttpResponseRedirect(request.GET.get("next"))
121121

122122
# Ensure that subclasses are still vuln to XSS
123123
def tp_not_found(request):

0 commit comments

Comments
 (0)