Skip to content

Commit c69b776

Browse files
authored
Merge pull request github#4864 from RasmusWL/django-request-handler-without-route
Python: Model Django request handler without route
2 parents fa8e902 + 2ba7ed4 commit c69b776

File tree

16 files changed

+233
-117
lines changed

16 files changed

+233
-117
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 handlers on `View` classes without known route, thereby leading to more sources of remote user input (`RemoteFlowSource`).

python/ql/src/semmle/python/Concepts.qll

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -313,8 +313,12 @@ module HTTP {
313313
/** Gets the URL pattern for this route, if it can be statically determined. */
314314
string getUrlPattern() { result = range.getUrlPattern() }
315315

316-
/** Gets a function that will handle incoming requests for this route, if any. */
317-
Function getARouteHandler() { result = range.getARouteHandler() }
316+
/**
317+
* Gets a function that will handle incoming requests for this route, if any.
318+
*
319+
* NOTE: This will be modified in the near future to have a `RequestHandler` result, instead of a `Function`.
320+
*/
321+
Function getARequestHandler() { result = range.getARequestHandler() }
318322

319323
/**
320324
* Gets a parameter that will receive parts of the url when handling incoming
@@ -343,8 +347,12 @@ module HTTP {
343347
)
344348
}
345349

346-
/** Gets a function that will handle incoming requests for this route, if any. */
347-
abstract Function getARouteHandler();
350+
/**
351+
* Gets a function that will handle incoming requests for this route, if any.
352+
*
353+
* NOTE: This will be modified in the near future to have a `RequestHandler` result, instead of a `Function`.
354+
*/
355+
abstract Function getARequestHandler();
348356

349357
/**
350358
* Gets a parameter that will receive parts of the url when handling incoming
@@ -354,8 +362,57 @@ module HTTP {
354362
}
355363
}
356364

365+
/**
366+
* A function that will handle incoming HTTP requests.
367+
*
368+
* Extend this class to refine existing API models. If you want to model new APIs,
369+
* extend `RequestHandler::Range` instead.
370+
*/
371+
class RequestHandler extends Function {
372+
RequestHandler::Range range;
373+
374+
RequestHandler() { this = range }
375+
376+
/**
377+
* Gets a parameter that could receive parts of the url when handling incoming
378+
* requests, if any. These automatically become a `RemoteFlowSource`.
379+
*/
380+
Parameter getARoutedParameter() { result = range.getARoutedParameter() }
381+
}
382+
383+
/** Provides a class for modeling new HTTP request handlers. */
384+
module RequestHandler {
385+
/**
386+
* A function that will handle incoming HTTP requests.
387+
*
388+
* Extend this class to model new APIs. If you want to refine existing API models,
389+
* extend `RequestHandler` instead.
390+
*
391+
* Only extend this class if you can't provide a `RouteSetup`, since we handle that case automatically.
392+
*/
393+
abstract class Range extends Function {
394+
/**
395+
* Gets a parameter that could receive parts of the url when handling incoming
396+
* requests, if any. These automatically become a `RemoteFlowSource`.
397+
*/
398+
abstract Parameter getARoutedParameter();
399+
}
400+
}
401+
402+
private class RequestHandlerFromRouteSetup extends RequestHandler::Range {
403+
RouteSetup rs;
404+
405+
RequestHandlerFromRouteSetup() { this = rs.getARequestHandler() }
406+
407+
override Parameter getARoutedParameter() {
408+
result = rs.getARoutedParameter() and
409+
result in [this.getArg(_), this.getArgByName(_)]
410+
}
411+
}
412+
413+
/** A parameter that will receive parts of the url when handling an incoming request. */
357414
private class RoutedParameter extends RemoteFlowSource::Range, DataFlow::ParameterNode {
358-
RoutedParameter() { this.getParameter() = any(RouteSetup setup).getARoutedParameter() }
415+
RoutedParameter() { this.getParameter() = any(RequestHandler handler).getARoutedParameter() }
359416

360417
override string getSourceType() { result = "RoutedParameter" }
361418
}

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

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1676,7 +1676,7 @@ private module Django {
16761676
DjangoViewClassDef() { this.getABase() = django::views::generic::View::subclassRef().asExpr() }
16771677

16781678
/** Gets a function that could handle incoming requests, if any. */
1679-
DjangoRouteHandler getARouteHandler() {
1679+
DjangoRouteHandler getARequestHandler() {
16801680
// TODO: This doesn't handle attribute assignment. Should be OK, but analysis is not as complete as with
16811681
// points-to and `.lookup`, which would handle `post = my_post_handler` inside class def
16821682
result = this.getAMethod() and
@@ -1725,7 +1725,7 @@ private module Django {
17251725
DjangoRouteHandler() {
17261726
exists(djangoRouteHandlerFunctionTracker(this))
17271727
or
1728-
any(DjangoViewClassDef vc).getARouteHandler() = this
1728+
any(DjangoViewClassDef vc).getARequestHandler() = this
17291729
}
17301730

17311731
/** Gets the index of the request parameter. */
@@ -1746,16 +1746,33 @@ private module Django {
17461746
/** Gets the data-flow node that is used as the argument for the view handler. */
17471747
abstract DataFlow::Node getViewArg();
17481748

1749-
final override DjangoRouteHandler getARouteHandler() {
1749+
final override DjangoRouteHandler getARequestHandler() {
17501750
djangoRouteHandlerFunctionTracker(result) = getViewArg()
17511751
or
17521752
exists(DjangoViewClassDef vc |
17531753
getViewArg() = vc.asViewResult() and
1754-
result = vc.getARouteHandler()
1754+
result = vc.getARequestHandler()
17551755
)
17561756
}
17571757
}
17581758

1759+
/** A request handler defined in a django view class, that has no known route. */
1760+
private class DjangoViewClassHandlerWithoutKnownRoute extends HTTP::Server::RequestHandler::Range,
1761+
DjangoRouteHandler {
1762+
DjangoViewClassHandlerWithoutKnownRoute() {
1763+
exists(DjangoViewClassDef vc | vc.getARequestHandler() = this) and
1764+
not exists(DjangoRouteSetup setup | setup.getARequestHandler() = this)
1765+
}
1766+
1767+
override Parameter getARoutedParameter() {
1768+
// Since we don't know the URL pattern, we simply mark all parameters as a routed
1769+
// parameter. This should give us more RemoteFlowSources but could also lead to
1770+
// more FPs. If this turns out to be the wrong tradeoff, we can always change our mind.
1771+
result in [this.getArg(_), this.getArgByName(_)] and
1772+
not result = any(int i | i <= this.getRequestParamIndex() | this.getArg(i))
1773+
}
1774+
}
1775+
17591776
/**
17601777
* Gets the regex that is used by django to find routed parameters when using `django.urls.path`.
17611778
*
@@ -1787,14 +1804,14 @@ private module Django {
17871804
// If we don't know the URL pattern, we simply mark all parameters as a routed
17881805
// parameter. This should give us more RemoteFlowSources but could also lead to
17891806
// more FPs. If this turns out to be the wrong tradeoff, we can always change our mind.
1790-
exists(DjangoRouteHandler routeHandler | routeHandler = this.getARouteHandler() |
1807+
exists(DjangoRouteHandler routeHandler | routeHandler = this.getARequestHandler() |
17911808
not exists(this.getUrlPattern()) and
17921809
result in [routeHandler.getArg(_), routeHandler.getArgByName(_)] and
17931810
not result = any(int i | i <= routeHandler.getRequestParamIndex() | routeHandler.getArg(i))
17941811
)
17951812
or
17961813
exists(string name |
1797-
result = this.getARouteHandler().getArgByName(name) and
1814+
result = this.getARequestHandler().getArgByName(name) and
17981815
exists(string match |
17991816
match = this.getUrlPattern().regexpFind(pathRoutedParameterRegex(), _, _) and
18001817
name = match.regexpCapture(pathRoutedParameterRegex(), 2)
@@ -1809,14 +1826,14 @@ private module Django {
18091826
// If we don't know the URL pattern, we simply mark all parameters as a routed
18101827
// parameter. This should give us more RemoteFlowSources but could also lead to
18111828
// more FPs. If this turns out to be the wrong tradeoff, we can always change our mind.
1812-
exists(DjangoRouteHandler routeHandler | routeHandler = this.getARouteHandler() |
1829+
exists(DjangoRouteHandler routeHandler | routeHandler = this.getARequestHandler() |
18131830
not exists(this.getUrlPattern()) and
18141831
result in [routeHandler.getArg(_), routeHandler.getArgByName(_)] and
18151832
not result = any(int i | i <= routeHandler.getRequestParamIndex() | routeHandler.getArg(i))
18161833
)
18171834
or
18181835
exists(DjangoRouteHandler routeHandler, DjangoRouteRegex regex |
1819-
routeHandler = this.getARouteHandler() and
1836+
routeHandler = this.getARequestHandler() and
18201837
regex.getRouteSetup() = this
18211838
|
18221839
// either using named capture groups (passed as keyword arguments) or using
@@ -1888,10 +1905,13 @@ private module Django {
18881905
// ---------------------------------------------------------------------------
18891906
// HttpRequest taint modeling
18901907
// ---------------------------------------------------------------------------
1891-
class DjangoRouteHandlerRequestParam extends django::http::request::HttpRequest::InstanceSource,
1908+
/** A parameter that will receive the django `HttpRequest` instance when a request handler is invoked. */
1909+
private class DjangoRequestHandlerRequestParam extends django::http::request::HttpRequest::InstanceSource,
18921910
RemoteFlowSource::Range, DataFlow::ParameterNode {
1893-
DjangoRouteHandlerRequestParam() {
1894-
this.getParameter() = any(DjangoRouteSetup setup).getARouteHandler().getRequestParam()
1911+
DjangoRequestHandlerRequestParam() {
1912+
this.getParameter() = any(DjangoRouteSetup setup).getARequestHandler().getRequestParam()
1913+
or
1914+
this.getParameter() = any(DjangoViewClassHandlerWithoutKnownRoute setup).getRequestParam()
18951915
}
18961916

18971917
override string getSourceType() { result = "django.http.request.HttpRequest" }

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -275,10 +275,10 @@ private module FlaskModel {
275275
// parameter. This should give us more RemoteFlowSources but could also lead to
276276
// more FPs. If this turns out to be the wrong tradeoff, we can always change our mind.
277277
not exists(this.getUrlPattern()) and
278-
result = this.getARouteHandler().getArgByName(_)
278+
result = this.getARequestHandler().getArgByName(_)
279279
or
280280
exists(string name |
281-
result = this.getARouteHandler().getArgByName(name) and
281+
result = this.getARequestHandler().getArgByName(name) and
282282
exists(string match |
283283
match = this.getUrlPattern().regexpFind(werkzeug_rule_re(), _, _) and
284284
name = match.regexpCapture(werkzeug_rule_re(), 4)
@@ -301,7 +301,7 @@ private module FlaskModel {
301301
result.asCfgNode() in [node.getArg(0), node.getArgByName("rule")]
302302
}
303303

304-
override Function getARouteHandler() { result.getADecorator().getAFlowNode() = node }
304+
override Function getARequestHandler() { result.getADecorator().getAFlowNode() = node }
305305
}
306306

307307
/**
@@ -318,7 +318,7 @@ private module FlaskModel {
318318
result.asCfgNode() in [node.getArg(0), node.getArgByName("rule")]
319319
}
320320

321-
override Function getARouteHandler() {
321+
override Function getARequestHandler() {
322322
exists(DataFlow::Node view_func_arg, DataFlow::LocalSourceNode func_src |
323323
view_func_arg.asCfgNode() in [node.getArg(2), node.getArgByName("view_func")] and
324324
func_src.flowsTo(view_func_arg) and
@@ -484,7 +484,7 @@ private module FlaskModel {
484484
private class FlaskRouteHandlerReturn extends HTTP::Server::HttpResponse::Range, DataFlow::CfgNode {
485485
FlaskRouteHandlerReturn() {
486486
exists(Function routeHandler |
487-
routeHandler = any(FlaskRouteSetup rs).getARouteHandler() and
487+
routeHandler = any(FlaskRouteSetup rs).getARequestHandler() and
488488
node = routeHandler.getAReturnValueFlowNode()
489489
)
490490
}

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1616,6 +1616,20 @@ private module Stdlib {
16161616
)
16171617
}
16181618
}
1619+
1620+
/**
1621+
* The entry-point for handling a request with a `BaseHTTPRequestHandler` subclass.
1622+
*
1623+
* Not essential for any functionality, but provides a consistent modeling.
1624+
*/
1625+
private class RequestHandlerFunc extends HTTP::Server::RequestHandler::Range {
1626+
RequestHandlerFunc() {
1627+
this = any(HTTPRequestHandlerClassDef cls).getAMethod() and
1628+
this.getName() = "do_" + HTTP::httpVerb()
1629+
}
1630+
1631+
override Parameter getARoutedParameter() { none() }
1632+
}
16191633
}
16201634

16211635
// ---------------------------------------------------------------------------

python/ql/src/semmle/python/web/HttpConstants.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** Gets an HTTP verb */
1+
/** Gets an HTTP verb, in upper case */
22
string httpVerb() {
33
result = "GET" or
44
result = "POST" or

python/ql/test/experimental/library-tests/frameworks/django-v1/routing_test.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,19 @@
44
from django.views.generic import View
55

66

7-
def url_match_xss(request, foo, bar, no_taint=None): # $routeHandler routedParameter=foo routedParameter=bar
7+
def url_match_xss(request, foo, bar, no_taint=None): # $requestHandler routedParameter=foo routedParameter=bar
88
return HttpResponse('url_match_xss: {} {}'.format(foo, bar)) # $HttpResponse
99

1010

11-
def get_params_xss(request): # $routeHandler
11+
def get_params_xss(request): # $requestHandler
1212
return HttpResponse(request.GET.get("untrusted")) # $HttpResponse
1313

1414

15-
def post_params_xss(request): # $routeHandler
15+
def post_params_xss(request): # $requestHandler
1616
return HttpResponse(request.POST.get("untrusted")) # $HttpResponse
1717

1818

19-
def http_resp_write(request): # $routeHandler
19+
def http_resp_write(request): # $requestHandler
2020
rsp = HttpResponse() # $HttpResponse
2121
rsp.write(request.GET.get("untrusted")) # $HttpResponse
2222
return rsp
@@ -26,22 +26,22 @@ 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

2828

29-
def post(self, request, untrusted): # $ MISSING: routeHandler routedParameter=untrusted
29+
def post(self, request, untrusted): # $ MISSING: requestHandler routedParameter=untrusted
3030
return HttpResponse('Foo post: {}'.format(untrusted)) # $HttpResponse
3131

3232

3333
class ClassView(View, Foo):
3434

35-
def get(self, request, untrusted): # $ routeHandler routedParameter=untrusted
35+
def get(self, request, untrusted): # $ requestHandler routedParameter=untrusted
3636
return HttpResponse('ClassView get: {}'.format(untrusted)) # $HttpResponse
3737

3838

39-
def show_articles(request, page_number=1): # $routeHandler routedParameter=page_number
39+
def show_articles(request, page_number=1): # $requestHandler routedParameter=page_number
4040
page_number = int(page_number)
4141
return HttpResponse('articles page: {}'.format(page_number)) # $HttpResponse
4242

4343

44-
def xxs_positional_arg(request, arg0, arg1, no_taint=None): # $routeHandler routedParameter=arg0 routedParameter=arg1
44+
def xxs_positional_arg(request, arg0, arg1, no_taint=None): # $requestHandler routedParameter=arg0 routedParameter=arg1
4545
return HttpResponse('xxs_positional_arg: {} {}'.format(arg0, arg1)) # $HttpResponse
4646

4747

@@ -62,7 +62,7 @@ def xxs_positional_arg(request, arg0, arg1, no_taint=None): # $routeHandler rou
6262
################################################################################
6363
# Using patterns() for routing
6464

65-
def show_user(request, username): # $routeHandler routedParameter=username
65+
def show_user(request, username): # $requestHandler routedParameter=username
6666
return HttpResponse('show_user {}'.format(username)) # $HttpResponse
6767

6868

@@ -71,7 +71,7 @@ def show_user(request, username): # $routeHandler routedParameter=username
7171
################################################################################
7272
# Show we understand the keyword arguments to django.conf.urls.url
7373

74-
def kw_args(request): # $routeHandler
74+
def kw_args(request): # $requestHandler
7575
return HttpResponse('kw_args') # $HttpResponse
7676

7777
urlpatterns = [

0 commit comments

Comments
 (0)