Skip to content

Commit e3b3825

Browse files
authored
Merge pull request #5151 from RasmusWL/django-get-redirect-url
Python: Model get_redirect_url in django
2 parents 0c4a5f5 + 7451484 commit e3b3825

File tree

6 files changed

+97
-6
lines changed

6 files changed

+97
-6
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lgtm,codescanning
2+
* Improved modeling of `django` to recognize request redirects from `get_redirect_url` on a `RedirectView` subclass.

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

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2074,7 +2074,11 @@ private module Django {
20742074
// TODO: This doesn't handle attribute assignment. Should be OK, but analysis is not as complete as with
20752075
// points-to and `.lookup`, which would handle `post = my_post_handler` inside class def
20762076
result = this.getAMethod() and
2077-
result.getName() = HTTP::httpVerbLower()
2077+
(
2078+
result.getName() = HTTP::httpVerbLower()
2079+
or
2080+
result.getName() = "get_redirect_url"
2081+
)
20782082
}
20792083

20802084
/**
@@ -2124,6 +2128,8 @@ private module Django {
21242128
/**
21252129
* A function that is a django route handler, meaning it handles incoming requests
21262130
* with the django framework.
2131+
*
2132+
* Most functions take a django HttpRequest as a parameter (but not all).
21272133
*/
21282134
private class DjangoRouteHandler extends Function {
21292135
DjangoRouteHandler() {
@@ -2132,6 +2138,12 @@ private module Django {
21322138
any(DjangoViewClass vc).getARequestHandler() = this
21332139
}
21342140

2141+
/**
2142+
* Gets the index of the parameter where the first routed parameter can be passed --
2143+
* that is, the one just after any possible `self` or HttpRequest parameters.
2144+
*/
2145+
int getFirstPossibleRoutedParamIndex() { result = 1 + this.getRequestParamIndex() }
2146+
21352147
/** Gets the index of the request parameter. */
21362148
int getRequestParamIndex() {
21372149
not this.isMethod() and
@@ -2145,6 +2157,26 @@ private module Django {
21452157
Parameter getRequestParam() { result = this.getArg(this.getRequestParamIndex()) }
21462158
}
21472159

2160+
/**
2161+
* A method named `get_redirect_url` on a django view class.
2162+
*
2163+
* See https://docs.djangoproject.com/en/3.1/ref/class-based-views/base/#django.views.generic.base.RedirectView.get_redirect_url
2164+
*
2165+
* Note: this function only does something on a subclass of `RedirectView`, but since
2166+
* classes can be considered django view classes without us knowing their super-classes,
2167+
* we need to consider _any_ django view class. I don't expect any problems to come from this.
2168+
*/
2169+
private class GetRedirectUrlFunction extends DjangoRouteHandler {
2170+
GetRedirectUrlFunction() {
2171+
this.getName() = "get_redirect_url" and
2172+
any(DjangoViewClass vc).getARequestHandler() = this
2173+
}
2174+
2175+
override int getFirstPossibleRoutedParamIndex() { result = 1 }
2176+
2177+
override int getRequestParamIndex() { none() }
2178+
}
2179+
21482180
/** A data-flow node that sets up a route on a server, using the django framework. */
21492181
abstract private class DjangoRouteSetup extends HTTP::Server::RouteSetup::Range, DataFlow::CfgNode {
21502182
/** Gets the data-flow node that is used as the argument for the view handler. */
@@ -2175,7 +2207,7 @@ private module Django {
21752207
// parameter. This should give us more RemoteFlowSources but could also lead to
21762208
// more FPs. If this turns out to be the wrong tradeoff, we can always change our mind.
21772209
result in [this.getArg(_), this.getArgByName(_)] and
2178-
not result = any(int i | i <= this.getRequestParamIndex() | this.getArg(i))
2210+
not result = any(int i | i < this.getFirstPossibleRoutedParamIndex() | this.getArg(i))
21792211
}
21802212

21812213
override string getFramework() { result = "Django" }
@@ -2215,7 +2247,8 @@ private module Django {
22152247
exists(DjangoRouteHandler routeHandler | routeHandler = this.getARequestHandler() |
22162248
not exists(this.getUrlPattern()) and
22172249
result in [routeHandler.getArg(_), routeHandler.getArgByName(_)] and
2218-
not result = any(int i | i <= routeHandler.getRequestParamIndex() | routeHandler.getArg(i))
2250+
not result =
2251+
any(int i | i < routeHandler.getFirstPossibleRoutedParamIndex() | routeHandler.getArg(i))
22192252
)
22202253
or
22212254
exists(string name |
@@ -2237,7 +2270,8 @@ private module Django {
22372270
exists(DjangoRouteHandler routeHandler | routeHandler = this.getARequestHandler() |
22382271
not exists(this.getUrlPattern()) and
22392272
result in [routeHandler.getArg(_), routeHandler.getArgByName(_)] and
2240-
not result = any(int i | i <= routeHandler.getRequestParamIndex() | routeHandler.getArg(i))
2273+
not result =
2274+
any(int i | i < routeHandler.getFirstPossibleRoutedParamIndex() | routeHandler.getArg(i))
22412275
)
22422276
or
22432277
exists(DjangoRouteHandler routeHandler, DjangoRouteRegex regex |
@@ -2249,7 +2283,9 @@ private module Django {
22492283
not exists(regex.getGroupName(_, _)) and
22502284
// first group will have group number 1
22512285
result =
2252-
routeHandler.getArg(routeHandler.getRequestParamIndex() + regex.getGroupNumber(_, _))
2286+
routeHandler
2287+
.getArg(routeHandler.getFirstPossibleRoutedParamIndex() - 1 +
2288+
regex.getGroupNumber(_, _))
22532289
or
22542290
result = routeHandler.getArgByName(regex.getGroupName(_, _))
22552291
)
@@ -2445,4 +2481,31 @@ private module Django {
24452481

24462482
override string getMimetypeDefault() { none() }
24472483
}
2484+
2485+
// ---------------------------------------------------------------------------
2486+
// RedirectView handling
2487+
// ---------------------------------------------------------------------------
2488+
/**
2489+
* A return from a method named `get_redirect_url` on a django view class.
2490+
*
2491+
* Note that in reality, this only does something on a subclass of `RedirectView` --
2492+
* but until API graphs makes this easy to model, I took a shortcut in modeling
2493+
* preciseness.
2494+
*
2495+
* See https://docs.djangoproject.com/en/3.1/ref/class-based-views/base/#redirectview
2496+
*/
2497+
private class DjangoRedirectViewGetRedirectUrlReturn extends HTTP::Server::HttpRedirectResponse::Range,
2498+
DataFlow::CfgNode {
2499+
DjangoRedirectViewGetRedirectUrlReturn() {
2500+
node = any(GetRedirectUrlFunction f).getAReturnValueFlowNode()
2501+
}
2502+
2503+
override DataFlow::Node getRedirectLocation() { result = this }
2504+
2505+
override DataFlow::Node getBody() { none() }
2506+
2507+
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
2508+
2509+
override string getMimetypeDefault() { none() }
2510+
}
24482511
}

python/ql/test/experimental/library-tests/frameworks/django-v2-v3/TestTaint.expected

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
| response_test.py:61 | ok | get_redirect_url | foo |
12
| taint_test.py:8 | ok | test_taint | bar |
23
| taint_test.py:8 | ok | test_taint | foo |
34
| taint_test.py:9 | ok | test_taint | baz |

python/ql/test/experimental/library-tests/frameworks/django-v1/response_test.py renamed to python/ql/test/experimental/library-tests/frameworks/django-v2-v3/response_test.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django.http.response import HttpResponse, HttpResponseRedirect, HttpResponsePermanentRedirect, JsonResponse, HttpResponseNotFound
2+
from django.views.generic import RedirectView
23
import django.shortcuts
34

45
# Not an XSS sink, since the Content-Type is not "text/html"
@@ -54,6 +55,14 @@ def redirect_shortcut(request):
5455
return django.shortcuts.redirect(next) # $ HttpResponse HttpRedirectResponse redirectLocation=next
5556

5657

58+
class CustomRedirectView(RedirectView):
59+
60+
def get_redirect_url(self, foo): # $ requestHandler routedParameter=foo
61+
ensure_tainted(foo)
62+
next = "https://example.com/{}".format(foo)
63+
return next # $ HttpResponse HttpRedirectResponse redirectLocation=next
64+
65+
5766
# Ensure that simple subclasses are still vuln to XSS
5867
def xss__not_found(request):
5968
return HttpResponseNotFound(request.GET.get("name")) # $HttpResponse mimetype=text/html responseBody=Attribute()

python/ql/test/experimental/library-tests/frameworks/django-v2-v3/testapp/urls.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,7 @@
1515

1616
path("basic-view-handler/", views.MyBasicViewHandler.as_view()), # $routeSetup="basic-view-handler/"
1717
path("custom-inheritance-view-handler/", views.MyViewHandlerWithCustomInheritance.as_view()), # $routeSetup="custom-inheritance-view-handler/"
18+
19+
path("CustomRedirectView/<foo>", views.CustomRedirectView.as_view()), # $routeSetup="CustomRedirectView/<foo>"
20+
path("CustomRedirectView2/<foo>", views.CustomRedirectView2.as_view()), # $routeSetup="CustomRedirectView2/<foo>"
1821
]

python/ql/test/experimental/library-tests/frameworks/django-v2-v3/testapp/views.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from django.http import HttpRequest, HttpResponse
2-
from django.views import View
2+
from django.views.generic import View, RedirectView
33
from django.views.decorators.csrf import csrf_exempt
44

55

@@ -32,3 +32,16 @@ class MyViewHandlerWithCustomInheritance(MyCustomViewBaseClass):
3232
def get(self, request: HttpRequest): # $ requestHandler
3333
print(self.request.GET)
3434
return HttpResponse("MyViewHandlerWithCustomInheritance: GET") # $ HttpResponse
35+
36+
# RedirectView
37+
# See docs at https://docs.djangoproject.com/en/3.1/ref/class-based-views/base/#redirectview
38+
class CustomRedirectView(RedirectView):
39+
40+
def get_redirect_url(self, foo): # $ requestHandler routedParameter=foo
41+
next = "https://example.com/{}".format(foo)
42+
return next # $ HttpResponse HttpRedirectResponse redirectLocation=next
43+
44+
45+
class CustomRedirectView2(RedirectView):
46+
47+
url = "https://example.com/%(foo)s"

0 commit comments

Comments
 (0)