Skip to content

Commit 7451484

Browse files
committed
Python: Model get_redirect_url in django
1 parent 6934d5e commit 7451484

File tree

5 files changed

+76
-9
lines changed

5 files changed

+76
-9
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. */
@@ -2173,7 +2205,7 @@ private module Django {
21732205
// parameter. This should give us more RemoteFlowSources but could also lead to
21742206
// more FPs. If this turns out to be the wrong tradeoff, we can always change our mind.
21752207
result in [this.getArg(_), this.getArgByName(_)] and
2176-
not result = any(int i | i <= this.getRequestParamIndex() | this.getArg(i))
2208+
not result = any(int i | i < this.getFirstPossibleRoutedParamIndex() | this.getArg(i))
21772209
}
21782210
}
21792211

@@ -2211,7 +2243,8 @@ private module Django {
22112243
exists(DjangoRouteHandler routeHandler | routeHandler = this.getARequestHandler() |
22122244
not exists(this.getUrlPattern()) and
22132245
result in [routeHandler.getArg(_), routeHandler.getArgByName(_)] and
2214-
not result = any(int i | i <= routeHandler.getRequestParamIndex() | routeHandler.getArg(i))
2246+
not result =
2247+
any(int i | i < routeHandler.getFirstPossibleRoutedParamIndex() | routeHandler.getArg(i))
22152248
)
22162249
or
22172250
exists(string name |
@@ -2233,7 +2266,8 @@ private module Django {
22332266
exists(DjangoRouteHandler routeHandler | routeHandler = this.getARequestHandler() |
22342267
not exists(this.getUrlPattern()) and
22352268
result in [routeHandler.getArg(_), routeHandler.getArgByName(_)] and
2236-
not result = any(int i | i <= routeHandler.getRequestParamIndex() | routeHandler.getArg(i))
2269+
not result =
2270+
any(int i | i < routeHandler.getFirstPossibleRoutedParamIndex() | routeHandler.getArg(i))
22372271
)
22382272
or
22392273
exists(DjangoRouteHandler routeHandler, DjangoRouteRegex regex |
@@ -2245,7 +2279,9 @@ private module Django {
22452279
not exists(regex.getGroupName(_, _)) and
22462280
// first group will have group number 1
22472281
result =
2248-
routeHandler.getArg(routeHandler.getRequestParamIndex() + regex.getGroupNumber(_, _))
2282+
routeHandler
2283+
.getArg(routeHandler.getFirstPossibleRoutedParamIndex() - 1 +
2284+
regex.getGroupNumber(_, _))
22492285
or
22502286
result = routeHandler.getArgByName(regex.getGroupName(_, _))
22512287
)
@@ -2441,4 +2477,31 @@ private module Django {
24412477

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

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-v2-v3/response_test.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,10 @@ def redirect_shortcut(request):
5757

5858
class CustomRedirectView(RedirectView):
5959

60-
def get_redirect_url(self, foo): # $ MISSING: routedParameter=foo
60+
def get_redirect_url(self, foo): # $ requestHandler routedParameter=foo
61+
ensure_tainted(foo)
6162
next = "https://example.com/{}".format(foo)
62-
return next # $ MISSING: HttpResponse HttpRedirectResponse redirectLocation=next
63+
return next # $ HttpResponse HttpRedirectResponse redirectLocation=next
6364

6465

6566
# Ensure that simple subclasses are still vuln to XSS

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ def get(self, request: HttpRequest): # $ requestHandler
3737
# See docs at https://docs.djangoproject.com/en/3.1/ref/class-based-views/base/#redirectview
3838
class CustomRedirectView(RedirectView):
3939

40-
def get_redirect_url(self, foo): # $ MISSING: routedParameter=foo
40+
def get_redirect_url(self, foo): # $ requestHandler routedParameter=foo
4141
next = "https://example.com/{}".format(foo)
42-
return next # $ MISSING: HttpResponse HttpRedirectResponse redirectLocation=next
42+
return next # $ HttpResponse HttpRedirectResponse redirectLocation=next
4343

4444

4545
class CustomRedirectView2(RedirectView):

0 commit comments

Comments
 (0)