Skip to content

Commit 2c7af72

Browse files
authored
Merge pull request github#2858 from RasmusWL/python-support-django2
Approved by tausbn
2 parents 888c504 + 6d72e77 commit 2c7af72

File tree

21 files changed

+419
-98
lines changed

21 files changed

+419
-98
lines changed

change-notes/1.24/analysis-python.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ The following changes in version 1.24 affect Python analysis in all applications
44

55
## General improvements
66

7+
Support for Django version 2.x and 3.x
8+
79
## New queries
810

911
| **Query** | **Tags** | **Purpose** |

python/ql/src/semmle/python/web/bottle/General.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class BottleRoute extends ControlFlowNode {
3636

3737
Function getFunction() { bottle_route(this, _, result) }
3838

39-
Parameter getNamedArgument() {
39+
Parameter getANamedArgument() {
4040
exists(string name, Function func |
4141
func = this.getFunction() and
4242
func.getArgByName(name) = result and

python/ql/src/semmle/python/web/bottle/Request.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class UntrustedFile extends TaintKind {
7171
/** Parameter to a bottle request handler function */
7272
class BottleRequestParameter extends HttpRequestTaintSource {
7373
BottleRequestParameter() {
74-
exists(BottleRoute route | route.getNamedArgument() = this.(ControlFlowNode).getNode())
74+
exists(BottleRoute route | route.getANamedArgument() = this.(ControlFlowNode).getNode())
7575
}
7676

7777
override predicate isSourceOf(TaintKind kind) { kind instanceof UntrustedStringKind }

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

Lines changed: 124 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,142 @@ import python
22
import semmle.python.regex
33
import semmle.python.web.Http
44

5-
predicate django_route(CallNode call, ControlFlowNode regex, FunctionValue view) {
6-
exists(FunctionValue url |
7-
Value::named("django.conf.urls.url") = url and
8-
url.getArgumentForCall(call, 0) = regex and
9-
url.getArgumentForCall(call, 1).pointsTo(view)
5+
// TODO: Since django uses `path = partial(...)`, our analysis doesn't understand this is
6+
// a FunctionValue, so we can't use `FunctionValue.getArgumentForCall`
7+
// https://github.com/django/django/blob/master/django/urls/conf.py#L76
8+
abstract class DjangoRoute extends CallNode {
9+
DjangoViewHandler getViewHandler() {
10+
result = view_handler_from_view_arg(this.getArg(1))
11+
or
12+
result = view_handler_from_view_arg(this.getArgByName("view"))
13+
}
14+
15+
abstract string getANamedArgument();
16+
17+
/**
18+
* Get the number of positional arguments that will be passed to the view.
19+
* Will only return a result if there are no named arguments.
20+
*/
21+
abstract int getNumPositionalArguments();
22+
}
23+
24+
/**
25+
* For function based views -- also see `DjangoClassBasedViewHandler`
26+
* https://docs.djangoproject.com/en/1.11/topics/http/views/
27+
* https://docs.djangoproject.com/en/3.0/topics/http/views/
28+
*/
29+
class DjangoViewHandler extends PythonFunctionValue {
30+
31+
/** Gets the index of the 'request' argument */
32+
int getRequestArgIndex() {
33+
result = 0
34+
}
35+
}
36+
37+
/**
38+
* Class based views
39+
* https://docs.djangoproject.com/en/1.11/topics/class-based-views/
40+
* https://docs.djangoproject.com/en/3.0/topics/class-based-views/
41+
*/
42+
private class DjangoViewClass extends ClassValue {
43+
DjangoViewClass() {
44+
Value::named("django.views.generic.View") = this.getASuperType()
45+
or
46+
Value::named("django.views.View") = this.getASuperType()
47+
}
48+
}
49+
50+
class DjangoClassBasedViewHandler extends DjangoViewHandler {
51+
DjangoClassBasedViewHandler() {
52+
exists(DjangoViewClass cls |
53+
cls.lookup(httpVerbLower()) = this
54+
)
55+
}
56+
57+
override int getRequestArgIndex() {
58+
// due to `self` being the first parameter
59+
result = 1
60+
}
61+
}
62+
63+
/**
64+
* Gets the function that will handle requests when `view_arg` is used as the view argument to a
65+
* django route. That is, this methods handles Class-based Views and its `as_view()` function.
66+
*/
67+
private DjangoViewHandler view_handler_from_view_arg(ControlFlowNode view_arg) {
68+
// Function-based view
69+
result = view_arg.pointsTo()
70+
or
71+
// Class-based view
72+
exists(ClassValue cls |
73+
cls = view_arg.(CallNode).getFunction().(AttrNode).getObject("as_view").pointsTo() and
74+
result = cls.lookup(httpVerbLower())
1075
)
1176
}
1277

78+
// We need this "dummy" class, since otherwise the regex argument would not be considered
79+
// a regex (RegexString is abstract)
1380
class DjangoRouteRegex extends RegexString {
14-
DjangoRouteRegex() { django_route(_, this.getAFlowNode(), _) }
81+
DjangoRouteRegex() { exists(DjangoRegexRoute route | route.getRouteArg() = this.getAFlowNode()) }
1582
}
1683

17-
class DjangoRoute extends CallNode {
18-
DjangoRoute() { django_route(this, _, _) }
84+
class DjangoRegexRoute extends DjangoRoute {
85+
ControlFlowNode route;
86+
87+
DjangoRegexRoute() {
88+
exists(FunctionValue route_maker |
89+
// Django 1.x: https://docs.djangoproject.com/en/1.11/ref/urls/#django.conf.urls.url
90+
Value::named("django.conf.urls.url") = route_maker and
91+
route_maker.getArgumentForCall(this, 0) = route
92+
)
93+
or
94+
// Django 2.x and 3.x: https://docs.djangoproject.com/en/3.0/ref/urls/#re-path
95+
this = Value::named("django.urls.re_path").getACall() and
96+
(
97+
route = this.getArg(0)
98+
or
99+
route = this.getArgByName("route")
100+
)
101+
}
19102

20-
FunctionValue getViewFunction() { django_route(this, _, result) }
103+
ControlFlowNode getRouteArg() { result = route }
21104

22-
string getNamedArgument() {
23-
exists(DjangoRouteRegex regex |
24-
django_route(this, regex.getAFlowNode(), _) and
25-
regex.getGroupName(_, _) = result
105+
override string getANamedArgument() {
106+
exists(DjangoRouteRegex regex | regex.getAFlowNode() = route |
107+
result = regex.getGroupName(_, _)
26108
)
27109
}
28110

29-
/**
30-
* Get the number of positional arguments that will be passed to the view.
31-
* Will only return a result if there are no named arguments.
32-
*/
33-
int getNumPositionalArguments() {
34-
exists(DjangoRouteRegex regex |
35-
django_route(this, regex.getAFlowNode(), _) and
36-
not exists(string s | s = regex.getGroupName(_, _)) and
111+
override int getNumPositionalArguments() {
112+
not exists(this.getANamedArgument()) and
113+
exists(DjangoRouteRegex regex | regex.getAFlowNode() = route |
37114
result = count(regex.getGroupNumber(_, _))
38115
)
39116
}
40117
}
118+
119+
class DjangoPathRoute extends DjangoRoute {
120+
ControlFlowNode route;
121+
122+
DjangoPathRoute() {
123+
// Django 2.x and 3.x: https://docs.djangoproject.com/en/3.0/ref/urls/#path
124+
this = Value::named("django.urls.path").getACall() and
125+
(
126+
route = this.getArg(0)
127+
or
128+
route = this.getArgByName("route")
129+
)
130+
}
131+
132+
override string getANamedArgument() {
133+
// regexp taken from django:
134+
// https://github.com/django/django/blob/7d1bf29977bb368d7c28e7c6eb146db3b3009ae7/django/urls/resolvers.py#L199
135+
exists(StrConst route_str, string match |
136+
route_str = route.getNode() and
137+
match = route_str.getText().regexpFind("<(?:(?<converter>[^>:]+):)?(?<parameter>\\w+)>", _, _) and
138+
result = match.regexpCapture("<(?:(?<converter>[^>:]+):)?(?<parameter>\\w+)>", 2)
139+
)
140+
}
141+
142+
override int getNumPositionalArguments() { none() }
143+
}

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@ class DjangoRedirect extends HttpRedirectTaintSink {
1717
override string toString() { result = "django.redirect" }
1818

1919
DjangoRedirect() {
20-
exists(CallNode call |
21-
redirect().getACall() = call and
22-
this = call.getAnArg()
23-
)
20+
this = redirect().getACall().getAnArg()
2421
}
2522
}

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

Lines changed: 16 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -39,54 +39,35 @@ class DjangoQueryDict extends TaintKind {
3939
}
4040
}
4141

42-
abstract class DjangoRequestSource extends HttpRequestTaintSource {
43-
override string toString() { result = "Django request source" }
44-
45-
override predicate isSourceOf(TaintKind kind) { kind instanceof DjangoRequest }
46-
}
47-
48-
/**
49-
* Function based views
50-
* https://docs.djangoproject.com/en/1.11/topics/http/views/
51-
*/
52-
private class DjangoFunctionBasedViewRequestArgument extends DjangoRequestSource {
53-
DjangoFunctionBasedViewRequestArgument() {
54-
exists(FunctionValue view |
55-
django_route(_, _, view) and
56-
this = view.getScope().getArg(0).asName().getAFlowNode()
42+
/** A Django request parameter */
43+
class DjangoRequestSource extends HttpRequestTaintSource {
44+
DjangoRequestSource() {
45+
exists(DjangoRoute route, DjangoViewHandler view, int request_arg_index |
46+
route.getViewHandler() = view and
47+
request_arg_index = view.getRequestArgIndex() and
48+
this = view.getScope().getArg(request_arg_index).asName().getAFlowNode()
5749
)
5850
}
59-
}
6051

61-
/**
62-
* Class based views
63-
* https://docs.djangoproject.com/en/1.11/topics/class-based-views/
64-
*/
65-
private class DjangoView extends ClassValue {
66-
DjangoView() { Value::named("django.views.generic.View") = this.getASuperType() }
67-
}
68-
69-
private FunctionValue djangoViewHttpMethod() {
70-
exists(DjangoView view | view.lookup(httpVerbLower()) = result)
71-
}
52+
override string toString() { result = "Django request source" }
7253

73-
class DjangoClassBasedViewRequestArgument extends DjangoRequestSource {
74-
DjangoClassBasedViewRequestArgument() {
75-
this = djangoViewHttpMethod().getScope().getArg(1).asName().getAFlowNode()
76-
}
54+
override predicate isSourceOf(TaintKind kind) { kind instanceof DjangoRequest }
7755
}
7856

7957
/** An argument specified in a url routing table */
8058
class DjangoRequestParameter extends HttpRequestTaintSource {
8159
DjangoRequestParameter() {
82-
exists(DjangoRoute route, Function f |
83-
f = route.getViewFunction().getScope() |
84-
this.(ControlFlowNode).getNode() = f.getArgByName(route.getNamedArgument())
60+
exists(DjangoRoute route, Function f, DjangoViewHandler view, int request_arg_index |
61+
route.getViewHandler() = view and
62+
request_arg_index = view.getRequestArgIndex() and
63+
f = view.getScope()
64+
|
65+
this.(ControlFlowNode).getNode() = f.getArgByName(route.getANamedArgument())
8566
or
8667
exists(int i | i >= 0 |
8768
i < route.getNumPositionalArguments() and
8869
// +1 because first argument is always the request
89-
this.(ControlFlowNode).getNode() = f.getArg(i+1)
70+
this.(ControlFlowNode).getNode() = f.getArg(request_arg_index + 1 + i)
9071
)
9172
)
9273
}

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,15 @@ class DjangoResponse extends TaintKind {
1414
}
1515

1616
private ClassValue theDjangoHttpResponseClass() {
17-
result = Value::named("django.http.response.HttpResponse") and
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???
1826
not result = theDjangoHttpRedirectClass()
1927
}
2028

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,9 @@ import python
44
FunctionValue redirect() { result = Value::named("django.shortcuts.redirect") }
55

66
ClassValue theDjangoHttpRedirectClass() {
7+
// version 1.x
78
result = Value::named("django.http.response.HttpResponseRedirectBase")
9+
or
10+
// version 2.x
11+
result = Value::named("django.http.HttpResponseRedirectBase")
812
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
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 |
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import python
2+
import semmle.python.web.HttpRedirect
3+
import semmle.python.security.strings.Untrusted
4+
5+
from HttpRedirectTaintSink sink, TaintKind kind
6+
where sink.sinks(kind)
7+
select sink, kind

0 commit comments

Comments
 (0)