Skip to content

Commit 6d72e77

Browse files
committed
Python: Django: Handle Class-based views
1 parent b760b1f commit 6d72e77

File tree

5 files changed

+80
-47
lines changed

5 files changed

+80
-47
lines changed

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

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import semmle.python.web.Http
66
// a FunctionValue, so we can't use `FunctionValue.getArgumentForCall`
77
// https://github.com/django/django/blob/master/django/urls/conf.py#L76
88
abstract class DjangoRoute extends CallNode {
9-
FunctionValue getViewFunction() {
10-
result = this.getArg(1).pointsTo()
9+
DjangoViewHandler getViewHandler() {
10+
result = view_handler_from_view_arg(this.getArg(1))
1111
or
12-
result = this.getArgByName("view").pointsTo()
12+
result = view_handler_from_view_arg(this.getArgByName("view"))
1313
}
1414

1515
abstract string getANamedArgument();
@@ -21,6 +21,60 @@ abstract class DjangoRoute extends CallNode {
2121
abstract int getNumPositionalArguments();
2222
}
2323

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())
75+
)
76+
}
77+
2478
// We need this "dummy" class, since otherwise the regex argument would not be considered
2579
// a regex (RegexString is abstract)
2680
class DjangoRouteRegex extends RegexString {

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

Lines changed: 15 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -39,60 +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-
* https://docs.djangoproject.com/en/3.0/topics/http/views/
52-
*/
53-
private class DjangoFunctionBasedViewRequestArgument extends DjangoRequestSource {
54-
DjangoFunctionBasedViewRequestArgument() {
55-
exists(DjangoRoute route, FunctionValue view |
56-
route.getViewFunction() = view and
57-
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()
5849
)
5950
}
60-
}
6151

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

79-
class DjangoClassBasedViewRequestArgument extends DjangoRequestSource {
80-
DjangoClassBasedViewRequestArgument() {
81-
this = djangoViewHttpMethod().getScope().getArg(1).asName().getAFlowNode()
82-
}
54+
override predicate isSourceOf(TaintKind kind) { kind instanceof DjangoRequest }
8355
}
8456

8557
/** An argument specified in a url routing table */
8658
class DjangoRequestParameter extends HttpRequestTaintSource {
8759
DjangoRequestParameter() {
88-
exists(DjangoRoute route, Function f |
89-
f = route.getViewFunction().getScope() |
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+
|
9065
this.(ControlFlowNode).getNode() = f.getArgByName(route.getANamedArgument())
9166
or
9267
exists(int i | i >= 0 |
9368
i < route.getNumPositionalArguments() and
9469
// +1 because first argument is always the request
95-
this.(ControlFlowNode).getNode() = f.getArg(i+1)
70+
this.(ControlFlowNode).getNode() = f.getArg(request_arg_index + 1 + i)
9671
)
9772
)
9873
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
| views_1x.py:15:21:15:27 | request | django.request.HttpRequest |
1414
| views_1x.py:19:21:19:27 | request | django.request.HttpRequest |
1515
| views_1x.py:29:20:29:26 | request | django.request.HttpRequest |
16+
| views_1x.py:29:29:29:37 | untrusted | externally controlled string |
1617
| views_1x.py:35:19:35:25 | request | django.request.HttpRequest |
18+
| views_1x.py:35:28:35:36 | untrusted | externally controlled string |
1719
| views_1x.py:39:19:39:25 | request | django.request.HttpRequest |
1820
| views_1x.py:39:28:39:38 | page_number | externally controlled string |
1921
| views_1x.py:44:24:44:30 | request | django.request.HttpRequest |
@@ -29,7 +31,9 @@
2931
| views_2x_3x.py:15:21:15:27 | request | django.request.HttpRequest |
3032
| views_2x_3x.py:19:21:19:27 | request | django.request.HttpRequest |
3133
| views_2x_3x.py:29:20:29:26 | request | django.request.HttpRequest |
34+
| views_2x_3x.py:29:29:29:37 | untrusted | externally controlled string |
3235
| views_2x_3x.py:35:19:35:25 | request | django.request.HttpRequest |
36+
| views_2x_3x.py:35:28:35:36 | untrusted | externally controlled string |
3337
| views_2x_3x.py:39:19:39:25 | request | django.request.HttpRequest |
3438
| views_2x_3x.py:39:28:39:38 | page_number | externally controlled string |
3539
| views_2x_3x.py:44:24:44:30 | request | django.request.HttpRequest |

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ def http_resp_write(request):
2525
class Foo(object):
2626
# Note: since Foo is used as the super type in a class view, it will be able to handle requests.
2727

28-
# TODO: Currently we don't flag `untrusted` as a DjangoRequestParameter
28+
2929
def post(self, request, untrusted):
3030
return HttpResponse('Foo post: {}'.format(untrusted))
3131

3232

3333
class ClassView(View, Foo):
34-
# TODO: Currently we don't flag `untrusted` as a DjangoRequestParameter
34+
3535
def get(self, request, untrusted):
3636
return HttpResponse('ClassView get: {}'.format(untrusted))
3737

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ def http_resp_write(request):
2525
class Foo(object):
2626
# Note: since Foo is used as the super type in a class view, it will be able to handle requests.
2727

28-
# TODO: Currently we don't flag `untrusted` as a DjangoRequestParameter
28+
2929
def post(self, request, untrusted):
3030
return HttpResponse('Foo post: {}'.format(untrusted))
3131

3232

3333
class ClassView(View, Foo):
34-
# TODO: Currently we don't flag `untrusted` as a DjangoRequestParameter
34+
3535
def get(self, request, untrusted):
3636
return HttpResponse('ClassView get: {}'.format(untrusted))
3737

0 commit comments

Comments
 (0)