Skip to content

Commit 6cba2fe

Browse files
committed
Python: Model Django response sinks that are not vuln to XSS
Since HttpResponse is not *only* used for XSS, it is still valuable to know the content is send as part of the response. The *proper* solution to this problem of not all HttpResponses being vulnerable to XSS is probably to define a new abstract class in Http.qll called HttpResponseXSSVulnerableSink (or similar). I would like to model a few more libraries/frameworks before fully comitting to an approach though.
1 parent 3774310 commit 6cba2fe

File tree

5 files changed

+73
-45
lines changed

5 files changed

+73
-45
lines changed

python/ql/src/Security/CWE-079/ReflectedXss.ql

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@ class ReflectedXssConfiguration extends TaintTracking::Configuration {
2828
source instanceof HttpRequestTaintSource
2929
}
3030

31-
override predicate isSink(TaintTracking::Sink sink) { sink instanceof HttpResponseTaintSink }
31+
override predicate isSink(TaintTracking::Sink sink) {
32+
sink instanceof HttpResponseTaintSink and
33+
not sink instanceof DjangoResponseContent
34+
or
35+
sink instanceof DjangoResponseContentXSSVulnerable
36+
}
3237
}
3338

3439
from ReflectedXssConfiguration config, TaintedPathSource src, TaintedPathSink sink

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class DjangoShortcutsRedirectSink extends HttpRedirectTaintSink {
2727
class DjangoRedirectResponseSink extends HttpRedirectTaintSink {
2828
DjangoRedirectResponseSink() {
2929
exists(CallNode call |
30-
call = any(DjangoRedirectResponse rr).getACall()
30+
call = any(DjangoRedirectResponseClass cls).getACall()
3131
|
3232
this = call.getArg(0)
3333
or

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

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ private class DjangoResponseKind extends TaintKind {
1212
/** INTENRAL taint-source used for tracking a django response. */
1313
private class DjangoResponseSource extends TaintSource {
1414
DjangoResponseSource() {
15-
exists(DjangoXSSVulnerableResponse cls |
15+
exists(DjangoContentResponseClass cls |
1616
cls.getACall() = this
1717
)
1818
}
@@ -37,28 +37,39 @@ class DjangoResponseWrite extends HttpResponseTaintSink {
3737
override string toString() { result = "django.Response.write(...)" }
3838
}
3939

40-
/** An argument to initialization of a django response, which is vulnerable to external data (xss) */
40+
/**
41+
* An argument to initialization of a django response.
42+
*/
4143
class DjangoResponseContent extends HttpResponseTaintSink {
44+
DjangoContentResponseClass cls;
45+
CallNode call;
46+
4247
DjangoResponseContent() {
43-
exists(CallNode call, DjangoXSSVulnerableResponse cls |
44-
call = cls.getACall() and
45-
this = cls.getContentArg(call) and
46-
(
47-
not exists(cls.getContentTypeArg(call))
48-
or
49-
exists(StringValue s |
50-
cls.getContentTypeArg(call).pointsTo(s) and
51-
s.getText().indexOf("text/html") = 0
52-
)
53-
)
54-
)
48+
call = cls.getACall() and
49+
this = cls.getContentArg(call)
5550
}
5651

5752
override predicate sinks(TaintKind kind) { kind instanceof StringKind }
5853

5954
override string toString() { result = "django.Response(...)" }
6055
}
6156

57+
/**
58+
* An argument to initialization of a django response, which is vulnerable to external data (XSS).
59+
*/
60+
class DjangoResponseContentXSSVulnerable extends DjangoResponseContent {
61+
override DjangoXSSVulnerableResponseClass cls;
62+
63+
DjangoResponseContentXSSVulnerable() {
64+
not exists(cls.getContentTypeArg(call))
65+
or
66+
exists(StringValue s |
67+
cls.getContentTypeArg(call).pointsTo(s) and
68+
s.getText().indexOf("text/html") = 0
69+
)
70+
}
71+
}
72+
6273
class DjangoCookieSet extends CookieSet, CallNode {
6374
DjangoCookieSet() {
6475
any(DjangoResponseKind r).taints(this.getFunction().(AttrNode).getObject("set_cookie"))

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

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,28 @@
11
import python
22

3-
/** A Class that is a Django Response (subclass of `django.http.HttpResponse`). */
4-
class DjangoResponse extends ClassValue {
3+
/** A class that is a Django Redirect Response (subclass of `django.http.HttpResponseRedirectBase`). */
4+
class DjangoRedirectResponseClass extends ClassValue {
5+
DjangoRedirectResponseClass() {
6+
exists(ClassValue redirect_base |
7+
// version 1.x
8+
redirect_base = Value::named("django.http.response.HttpResponseRedirectBase")
9+
or
10+
// version 2.x and 3.x
11+
redirect_base = Value::named("django.http.HttpResponseRedirectBase")
12+
|
13+
this.getASuperType() = redirect_base
14+
)
15+
}
16+
}
17+
18+
/**
19+
* A class that is a Django Response, which can contain content.
20+
* A subclass of `django.http.HttpResponse` that is not a `DjangoRedirectResponseClass`.
21+
*/
22+
class DjangoContentResponseClass extends ClassValue {
523
ClassValue base;
624

7-
DjangoResponse() {
25+
DjangoContentResponseClass() {
826
(
927
// version 1.x
1028
base = Value::named("django.http.response.HttpResponse")
@@ -15,49 +33,39 @@ class DjangoResponse extends ClassValue {
1533
) and
1634
this.getASuperType() = base
1735
}
18-
}
1936

20-
/** A Class that is a Django Redirect Response (subclass of `django.http.HttpResponseRedirectBase`). */
21-
class DjangoRedirectResponse extends DjangoResponse {
22-
DjangoRedirectResponse() {
23-
exists(ClassValue redirect_base |
24-
// version 1.x
25-
redirect_base = Value::named("django.http.response.HttpResponseRedirectBase")
26-
or
27-
// version 2.x and 3.x
28-
redirect_base = Value::named("django.http.HttpResponseRedirectBase")
29-
|
30-
this.getASuperType() = redirect_base
31-
)
32-
}
37+
// The reason these two method are defined in this class (and not in the Sink
38+
// definition that uses this class), is that if we were to add support for
39+
// `django.http.response.HttpResponseNotAllowed` it would make much more sense to add
40+
// the custom logic in this class (or subclass), than to handle all of it in the sink
41+
// definition.
42+
43+
/** Gets the `content` argument of a `call` to the constructor */
44+
ControlFlowNode getContentArg(CallNode call) { none() }
45+
46+
/** Gets the `content_type` argument of a `call` to the constructor */
47+
ControlFlowNode getContentTypeArg(CallNode call) { none() }
3348
}
3449

3550
/** A Class that is a Django Response, and is vulnerable to XSS. */
36-
class DjangoXSSVulnerableResponse extends DjangoResponse {
37-
DjangoXSSVulnerableResponse() {
51+
class DjangoXSSVulnerableResponseClass extends DjangoContentResponseClass{
52+
DjangoXSSVulnerableResponseClass() {
3853
// We want to avoid FPs on subclasses that are not exposed to XSS, for example `JsonResponse`.
3954
// The easiest way is to disregard any subclass that has a special `__init__` method.
4055
// It's not guaranteed to remove all FPs, or not to generate FNs, but compared to our
4156
// previous implementation that would treat 0-th argument to _any_ subclass as a sink,
4257
// this gets us much closer to reality.
4358
this.lookup("__init__") = base.lookup("__init__") and
44-
not this instanceof DjangoRedirectResponse
59+
not this instanceof DjangoRedirectResponseClass
4560
}
4661

47-
// The reason these two method are defined in this class (and no in the Sink
48-
// definition that uses this class), is that if we were to add support for `HttpResponseNotAllowed`
49-
// it would make much more sense to add the custom logic in this class (or subclass), than to handle all of it
50-
// in the sink definition.
51-
52-
/** Gets the `content` argument of a `call` to the constructor */
53-
ControlFlowNode getContentArg(CallNode call) {
62+
override ControlFlowNode getContentArg(CallNode call) {
5463
result = call.getArg(0)
5564
or
5665
result = call.getArgByName("content")
5766
}
5867

59-
/** Gets the `content_type` argument of a `call` to the constructor */
60-
ControlFlowNode getContentTypeArg(CallNode call) {
68+
override ControlFlowNode getContentTypeArg(CallNode call) {
6169
result = call.getArg(1)
6270
or
6371
result = call.getArgByName("content_type")

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
| views_1x.py:45:25:45:70 | django.Response(...) | externally controlled string |
99
| views_1x.py:66:25:66:55 | django.Response(...) | externally controlled string |
1010
| views_1x.py:75:25:75:33 | django.Response(...) | externally controlled string |
11+
| views_1x.py:90:25:90:33 | django.Response(...) | externally controlled string |
12+
| views_1x.py:94:25:94:58 | django.Response(...) | externally controlled string |
1113
| views_1x.py:103:33:103:55 | django.Response(...) | externally controlled string |
1214
| views_1x.py:107:25:107:47 | django.Response(...) | externally controlled string |
1315
| views_2x_3x.py:8:25:8:63 | django.Response(...) | externally controlled string |
@@ -23,5 +25,7 @@
2325
| views_2x_3x.py:82:25:82:69 | django.Response(...) | externally controlled string |
2426
| views_2x_3x.py:85:25:85:64 | django.Response(...) | externally controlled string |
2527
| views_2x_3x.py:88:25:88:32 | django.Response(...) | externally controlled string |
28+
| views_2x_3x.py:111:25:111:33 | django.Response(...) | externally controlled string |
29+
| views_2x_3x.py:115:25:115:58 | django.Response(...) | externally controlled string |
2630
| views_2x_3x.py:124:33:124:55 | django.Response(...) | externally controlled string |
2731
| views_2x_3x.py:128:25:128:47 | django.Response(...) | externally controlled string |

0 commit comments

Comments
 (0)