Skip to content

Commit a08eb99

Browse files
authored
Merge pull request github#4779 from RasmusWL/django-class-based-handlers
Python: Add modeling of django class based view handlers
2 parents 5106d5d + 3e6296c commit a08eb99

File tree

9 files changed

+247
-23
lines changed

9 files changed

+247
-23
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lgtm,codescanning
2+
* Added modeling of django class based view handlers (subclasses of `django.views.generic.View`) as sources of remote user input (`RemoteFlowSource`).

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,8 @@ module SqlExecution {
295295

296296
/** Provides classes for modeling HTTP-related APIs. */
297297
module HTTP {
298+
import semmle.python.web.HttpConstants
299+
298300
/** Provides classes for modeling HTTP servers. */
299301
module Server {
300302
/**

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

Lines changed: 212 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1498,6 +1498,147 @@ private module Django {
14981498
}
14991499
}
15001500
}
1501+
1502+
// -------------------------------------------------------------------------
1503+
// django.views
1504+
// -------------------------------------------------------------------------
1505+
/** Gets a reference to the `django.views` module. */
1506+
DataFlow::Node views() { result = django_attr("views") }
1507+
1508+
/** Provides models for the `django.views` module */
1509+
module views {
1510+
/**
1511+
* Gets a reference to the attribute `attr_name` of the `django.views` module.
1512+
* WARNING: Only holds for a few predefined attributes.
1513+
*/
1514+
private DataFlow::Node views_attr(DataFlow::TypeTracker t, string attr_name) {
1515+
// for 1.11.x, see: https://github.com/django/django/blob/stable/1.11.x/django/views/__init__.py
1516+
attr_name in ["generic", "View"] and
1517+
(
1518+
t.start() and
1519+
result = DataFlow::importNode("django.views" + "." + attr_name)
1520+
or
1521+
t.startInAttr(attr_name) and
1522+
result = views()
1523+
)
1524+
or
1525+
// Due to bad performance when using normal setup with `views_attr(t2, attr_name).track(t2, t)`
1526+
// we have inlined that code and forced a join
1527+
exists(DataFlow::TypeTracker t2 |
1528+
exists(DataFlow::StepSummary summary |
1529+
views_attr_first_join(t2, attr_name, result, summary) and
1530+
t = t2.append(summary)
1531+
)
1532+
)
1533+
}
1534+
1535+
pragma[nomagic]
1536+
private predicate views_attr_first_join(
1537+
DataFlow::TypeTracker t2, string attr_name, DataFlow::Node res,
1538+
DataFlow::StepSummary summary
1539+
) {
1540+
DataFlow::StepSummary::step(views_attr(t2, attr_name), res, summary)
1541+
}
1542+
1543+
/**
1544+
* Gets a reference to the attribute `attr_name` of the `django.views` module.
1545+
* WARNING: Only holds for a few predefined attributes.
1546+
*/
1547+
private DataFlow::Node views_attr(string attr_name) {
1548+
result = views_attr(DataFlow::TypeTracker::end(), attr_name)
1549+
}
1550+
1551+
// -------------------------------------------------------------------------
1552+
// django.views.generic
1553+
// -------------------------------------------------------------------------
1554+
/** Gets a reference to the `django.views.generic` module. */
1555+
DataFlow::Node generic() { result = views_attr("generic") }
1556+
1557+
/** Provides models for the `django.views.generic` module */
1558+
module generic {
1559+
/**
1560+
* Gets a reference to the attribute `attr_name` of the `django.views.generic` module.
1561+
* WARNING: Only holds for a few predefined attributes.
1562+
*/
1563+
private DataFlow::Node generic_attr(DataFlow::TypeTracker t, string attr_name) {
1564+
// for 3.1.x see: https://github.com/django/django/blob/stable/3.1.x/django/views/generic/__init__.py
1565+
// same for 1.11.x see: https://github.com/django/django/blob/stable/1.11.x/django/views/generic/__init__.py
1566+
attr_name in [
1567+
"View", "TemplateView", "RedirectView", "ArchiveIndexView", "YearArchiveView",
1568+
"MonthArchiveView", "WeekArchiveView", "DayArchiveView", "TodayArchiveView",
1569+
"DateDetailView", "DetailView", "FormView", "CreateView", "UpdateView", "DeleteView",
1570+
"ListView", "GenericViewError"
1571+
] and
1572+
(
1573+
t.start() and
1574+
result = DataFlow::importNode("django.views.generic" + "." + attr_name)
1575+
or
1576+
t.startInAttr(attr_name) and
1577+
result = generic()
1578+
)
1579+
or
1580+
// Due to bad performance when using normal setup with `generic_attr(t2, attr_name).track(t2, t)`
1581+
// we have inlined that code and forced a join
1582+
exists(DataFlow::TypeTracker t2 |
1583+
exists(DataFlow::StepSummary summary |
1584+
generic_attr_first_join(t2, attr_name, result, summary) and
1585+
t = t2.append(summary)
1586+
)
1587+
)
1588+
}
1589+
1590+
pragma[nomagic]
1591+
private predicate generic_attr_first_join(
1592+
DataFlow::TypeTracker t2, string attr_name, DataFlow::Node res,
1593+
DataFlow::StepSummary summary
1594+
) {
1595+
DataFlow::StepSummary::step(generic_attr(t2, attr_name), res, summary)
1596+
}
1597+
1598+
/**
1599+
* Gets a reference to the attribute `attr_name` of the `django.views.generic` module.
1600+
* WARNING: Only holds for a few predefined attributes.
1601+
*/
1602+
private DataFlow::Node generic_attr(string attr_name) {
1603+
result = generic_attr(DataFlow::TypeTracker::end(), attr_name)
1604+
}
1605+
1606+
/**
1607+
* Provides models for the `django.views.generic.View` class and subclasses.
1608+
*
1609+
* See
1610+
* - https://docs.djangoproject.com/en/3.1/topics/class-based-views/
1611+
* - https://docs.djangoproject.com/en/3.1/ref/class-based-views/
1612+
*/
1613+
module View {
1614+
/** Gets a reference to the `django.views.generic.View` class or any subclass. */
1615+
private DataFlow::Node subclassRef(DataFlow::TypeTracker t) {
1616+
t.start() and
1617+
result =
1618+
generic_attr([
1619+
"View",
1620+
// Known Views
1621+
"TemplateView", "RedirectView", "ArchiveIndexView", "YearArchiveView",
1622+
"MonthArchiveView", "WeekArchiveView", "DayArchiveView", "TodayArchiveView",
1623+
"DateDetailView", "DetailView", "FormView", "CreateView", "UpdateView",
1624+
"DeleteView", "ListView"
1625+
])
1626+
or
1627+
// `django.views.View` alias
1628+
t.start() and
1629+
result = views_attr("View")
1630+
or
1631+
// subclasses in project code
1632+
result.asExpr().(ClassExpr).getABase() = subclassRef(t.continue()).asExpr()
1633+
or
1634+
exists(DataFlow::TypeTracker t2 | result = subclassRef(t2).track(t2, t))
1635+
}
1636+
1637+
/** Gets a reference to the `django.views.generic.View` class or any subclass. */
1638+
DataFlow::Node subclassRef() { result = subclassRef(DataFlow::TypeTracker::end()) }
1639+
}
1640+
}
1641+
}
15011642
}
15021643

15031644
// ---------------------------------------------------------------------------
@@ -1530,11 +1671,62 @@ private module Django {
15301671
result = djangoRouteHandlerFunctionTracker(DataFlow::TypeTracker::end(), func)
15311672
}
15321673

1674+
/** A django View class defined in project code. */
1675+
class DjangoViewClassDef extends Class {
1676+
DjangoViewClassDef() { this.getABase() = django::views::generic::View::subclassRef().asExpr() }
1677+
1678+
/** Gets a function that could handle incoming requests, if any. */
1679+
DjangoRouteHandler getARouteHandler() {
1680+
// TODO: This doesn't handle attribute assignment. Should be OK, but analysis is not as complete as with
1681+
// points-to and `.lookup`, which would handle `post = my_post_handler` inside class def
1682+
result = this.getAMethod() and
1683+
result.getName() = HTTP::httpVerbLower()
1684+
}
1685+
1686+
/** Gets a reference to this class. */
1687+
private DataFlow::Node getARef(DataFlow::TypeTracker t) {
1688+
t.start() and
1689+
result.asExpr().(ClassExpr) = this.getParent()
1690+
or
1691+
exists(DataFlow::TypeTracker t2 | result = this.getARef(t2).track(t2, t))
1692+
}
1693+
1694+
/** Gets a reference to this class. */
1695+
DataFlow::Node getARef() { result = this.getARef(DataFlow::TypeTracker::end()) }
1696+
1697+
/** Gets a reference to the `as_view` classmethod of this class. */
1698+
private DataFlow::Node asViewRef(DataFlow::TypeTracker t) {
1699+
t.startInAttr("as_view") and
1700+
result = this.getARef()
1701+
or
1702+
exists(DataFlow::TypeTracker t2 | result = this.asViewRef(t2).track(t2, t))
1703+
}
1704+
1705+
/** Gets a reference to the `as_view` classmethod of this class. */
1706+
DataFlow::Node asViewRef() { result = this.asViewRef(DataFlow::TypeTracker::end()) }
1707+
1708+
/** Gets a reference to the result of calling the `as_view` classmethod of this class. */
1709+
private DataFlow::Node asViewResult(DataFlow::TypeTracker t) {
1710+
t.start() and
1711+
result.asCfgNode().(CallNode).getFunction() = this.asViewRef().asCfgNode()
1712+
or
1713+
exists(DataFlow::TypeTracker t2 | result = asViewResult(t2).track(t2, t))
1714+
}
1715+
1716+
/** Gets a reference to the result of calling the `as_view` classmethod of this class. */
1717+
DataFlow::Node asViewResult() { result = asViewResult(DataFlow::TypeTracker::end()) }
1718+
}
1719+
15331720
/**
1534-
* A function that is used as a django route handler.
1721+
* A function that is a django route handler, meaning it handles incoming requests
1722+
* with the django framework.
15351723
*/
15361724
private class DjangoRouteHandler extends Function {
1537-
DjangoRouteHandler() { exists(djangoRouteHandlerFunctionTracker(this)) }
1725+
DjangoRouteHandler() {
1726+
exists(djangoRouteHandlerFunctionTracker(this))
1727+
or
1728+
any(DjangoViewClassDef vc).getARouteHandler() = this
1729+
}
15381730

15391731
/** Gets the index of the request parameter. */
15401732
int getRequestParamIndex() {
@@ -1549,8 +1741,19 @@ private module Django {
15491741
Parameter getRequestParam() { result = this.getArg(this.getRequestParamIndex()) }
15501742
}
15511743

1744+
/** A data-flow node that sets up a route on a server, using the django framework. */
15521745
abstract private class DjangoRouteSetup extends HTTP::Server::RouteSetup::Range, DataFlow::CfgNode {
1553-
abstract override DjangoRouteHandler getARouteHandler();
1746+
/** Gets the data-flow node that is used as the argument for the view handler. */
1747+
abstract DataFlow::Node getViewArg();
1748+
1749+
final override DjangoRouteHandler getARouteHandler() {
1750+
djangoRouteHandlerFunctionTracker(result) = getViewArg()
1751+
or
1752+
exists(DjangoViewClassDef vc |
1753+
getViewArg() = vc.asViewResult() and
1754+
result = vc.getARouteHandler()
1755+
)
1756+
}
15541757
}
15551758

15561759
/**
@@ -1576,11 +1779,8 @@ private module Django {
15761779
result.asCfgNode() = [node.getArg(0), node.getArgByName("route")]
15771780
}
15781781

1579-
override DjangoRouteHandler getARouteHandler() {
1580-
exists(DataFlow::Node viewArg |
1581-
viewArg.asCfgNode() in [node.getArg(1), node.getArgByName("view")] and
1582-
djangoRouteHandlerFunctionTracker(result) = viewArg
1583-
)
1782+
override DataFlow::Node getViewArg() {
1783+
result.asCfgNode() in [node.getArg(1), node.getArgByName("view")]
15841784
}
15851785

15861786
override Parameter getARoutedParameter() {
@@ -1661,11 +1861,8 @@ private module Django {
16611861
result.asCfgNode() = [node.getArg(0), node.getArgByName("route")]
16621862
}
16631863

1664-
override DjangoRouteHandler getARouteHandler() {
1665-
exists(DataFlow::Node viewArg |
1666-
viewArg.asCfgNode() in [node.getArg(1), node.getArgByName("view")] and
1667-
djangoRouteHandlerFunctionTracker(result) = viewArg
1668-
)
1864+
override DataFlow::Node getViewArg() {
1865+
result.asCfgNode() in [node.getArg(1), node.getArgByName("view")]
16691866
}
16701867
}
16711868

@@ -1683,11 +1880,8 @@ private module Django {
16831880
result.asCfgNode() = [node.getArg(0), node.getArgByName("regex")]
16841881
}
16851882

1686-
override DjangoRouteHandler getARouteHandler() {
1687-
exists(DataFlow::Node viewArg |
1688-
viewArg.asCfgNode() in [node.getArg(1), node.getArgByName("view")] and
1689-
djangoRouteHandlerFunctionTracker(result) = viewArg
1690-
)
1883+
override DataFlow::Node getViewArg() {
1884+
result.asCfgNode() in [node.getArg(1), node.getArgByName("view")]
16911885
}
16921886
}
16931887

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/** Gets an http verb */
1+
/** Gets an HTTP verb */
22
string httpVerb() {
33
result = "GET" or
44
result = "POST" or
@@ -9,5 +9,5 @@ string httpVerb() {
99
result = "HEAD"
1010
}
1111

12-
/** Gets an http verb, in lower case */
12+
/** Gets an HTTP verb, in lower case */
1313
string httpVerbLower() { result = httpVerb().toLowerCase() }

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def post(self, request, untrusted): # $ MISSING: routeHandler routedParameter=u
3232

3333
class ClassView(View, Foo):
3434

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

3838

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def post(self, request, untrusted): # $ MISSING: routeHandler routedParameter=u
3232

3333
class ClassView(View, Foo):
3434

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

3838

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
@@ -12,4 +12,7 @@
1212
# line)
1313
re_path(r"^ba[rz]/", views.bar_baz), # $routeSetup="^ba[rz]/"
1414
url(r"^deprecated/", views.deprecated), # $routeSetup="^deprecated/"
15+
16+
path("basic-view-handler/", views.MyBasicViewHandler.as_view()), # $routeSetup="basic-view-handler/"
17+
path("custom-inheritance-view-handler/", views.MyViewHandlerWithCustomInheritance.as_view()), # $routeSetup="custom-inheritance-view-handler/"
1518
]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,33 @@
11
from django.http import HttpRequest, HttpResponse
2+
from django.views import View
3+
from django.views.decorators.csrf import csrf_exempt
4+
25

36
def foo(request: HttpRequest): # $routeHandler
47
return HttpResponse("foo") # $HttpResponse
58

9+
610
def bar_baz(request: HttpRequest): # $routeHandler
711
return HttpResponse("bar_baz") # $HttpResponse
812

13+
914
def deprecated(request: HttpRequest): # $routeHandler
1015
return HttpResponse("deprecated") # $HttpResponse
16+
17+
18+
class MyBasicViewHandler(View):
19+
def get(self, request: HttpRequest): # $ routeHandler
20+
return HttpResponse("MyViewHandler: GET") # $ HttpResponse
21+
22+
def post(self, request: HttpRequest): # $ routeHandler
23+
return HttpResponse("MyViewHandler: POST") # $ HttpResponse
24+
25+
26+
class MyCustomViewBaseClass(View):
27+
def post(self, request: HttpRequest): # $ MISSING: routeHandler
28+
return HttpResponse("MyCustomViewBaseClass: POST") # $ HttpResponse
29+
30+
31+
class MyViewHandlerWithCustomInheritance(MyCustomViewBaseClass):
32+
def get(self, request: HttpRequest): # $ routeHandler
33+
return HttpResponse("MyViewHandlerWithCustomInheritance: GET") # $ HttpResponse

python/ql/test/experimental/library-tests/frameworks/django-v2-v3/testproj/settings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
'django.middleware.security.SecurityMiddleware',
4545
'django.contrib.sessions.middleware.SessionMiddleware',
4646
'django.middleware.common.CommonMiddleware',
47-
'django.middleware.csrf.CsrfViewMiddleware',
47+
# 'django.middleware.csrf.CsrfViewMiddleware',
4848
'django.contrib.auth.middleware.AuthenticationMiddleware',
4949
'django.contrib.messages.middleware.MessageMiddleware',
5050
'django.middleware.clickjacking.XFrameOptionsMiddleware',

0 commit comments

Comments
 (0)