Skip to content

Commit b28d022

Browse files
committed
Python: Add simpel model of a django path/re_path route setup
Also had to change the annotation to not include the `r` prefix for the raw-string... not sure why that isn't replicated, but ¯\_(ツ)_/¯
1 parent 979dc47 commit b28d022

File tree

6 files changed

+184
-28
lines changed

6 files changed

+184
-28
lines changed
Lines changed: 167 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,175 @@
11
/**
2-
* Provides classes modeling security-relevant aspects of the `django` package.
2+
* Provides classes modeling security-relevant aspects of the `django` PyPI package.
3+
* See https://www.djangoproject.com/.
34
*/
45

56
private import python
67
private import experimental.dataflow.DataFlow
78
private import experimental.dataflow.RemoteFlowSources
89
private import experimental.semmle.python.Concepts
910

10-
private module Django { }
11+
/**
12+
* Provides models for the `django` PyPI package.
13+
* See https://www.djangoproject.com/.
14+
*/
15+
private module Django {
16+
// ---------------------------------------------------------------------------
17+
// django
18+
// ---------------------------------------------------------------------------
19+
/** Gets a reference to the `django` module. */
20+
private DataFlow::Node django(DataFlow::TypeTracker t) {
21+
t.start() and
22+
result = DataFlow::importNode("django")
23+
or
24+
exists(DataFlow::TypeTracker t2 | result = django(t2).track(t2, t))
25+
}
26+
27+
/** Gets a reference to the `django` module. */
28+
DataFlow::Node django() { result = django(DataFlow::TypeTracker::end()) }
29+
30+
/**
31+
* Gets a reference to the attribute `attr_name` of the `django` module.
32+
* WARNING: Only holds for a few predefined attributes.
33+
*/
34+
private DataFlow::Node django_attr(DataFlow::TypeTracker t, string attr_name) {
35+
attr_name in ["urls"] and
36+
(
37+
t.start() and
38+
result = DataFlow::importNode("django" + "." + attr_name)
39+
or
40+
t.startInAttr(attr_name) and
41+
result = DataFlow::importNode("django")
42+
)
43+
or
44+
// Due to bad performance when using normal setup with `django_attr(t2, attr_name).track(t2, t)`
45+
// we have inlined that code and forced a join
46+
exists(DataFlow::TypeTracker t2 |
47+
exists(DataFlow::StepSummary summary |
48+
django_attr_first_join(t2, attr_name, result, summary) and
49+
t = t2.append(summary)
50+
)
51+
)
52+
}
53+
54+
pragma[nomagic]
55+
private predicate django_attr_first_join(
56+
DataFlow::TypeTracker t2, string attr_name, DataFlow::Node res, DataFlow::StepSummary summary
57+
) {
58+
DataFlow::StepSummary::step(django_attr(t2, attr_name), res, summary)
59+
}
60+
61+
/**
62+
* Gets a reference to the attribute `attr_name` of the `django` module.
63+
* WARNING: Only holds for a few predefined attributes.
64+
*/
65+
private DataFlow::Node django_attr(string attr_name) {
66+
result = django_attr(DataFlow::TypeTracker::end(), attr_name)
67+
}
68+
69+
/** Provides models for the `django` module. */
70+
module django {
71+
/** Gets a reference to the `django.urls` module. */
72+
DataFlow::Node urls() { result = django_attr("urls") }
73+
74+
// -------------------------------------------------------------------------
75+
// django.urls
76+
// -------------------------------------------------------------------------
77+
/** Provides models for the `django.urls` module */
78+
module urls {
79+
/**
80+
* Gets a reference to the attribute `attr_name` of the `urls` module.
81+
* WARNING: Only holds for a few predefined attributes.
82+
*/
83+
private DataFlow::Node urls_attr(DataFlow::TypeTracker t, string attr_name) {
84+
attr_name in ["path", "re_path"] and
85+
(
86+
t.start() and
87+
result = DataFlow::importNode("django.urls" + "." + attr_name)
88+
or
89+
t.startInAttr(attr_name) and
90+
result = DataFlow::importNode("django.urls")
91+
or
92+
t.startInAttr(attr_name) and
93+
result = django::urls()
94+
)
95+
or
96+
// Due to bad performance when using normal setup with `urls_attr(t2, attr_name).track(t2, t)`
97+
// we have inlined that code and forced a join
98+
exists(DataFlow::TypeTracker t2 |
99+
exists(DataFlow::StepSummary summary |
100+
urls_attr_first_join(t2, attr_name, result, summary) and
101+
t = t2.append(summary)
102+
)
103+
)
104+
}
105+
106+
pragma[nomagic]
107+
private predicate urls_attr_first_join(
108+
DataFlow::TypeTracker t2, string attr_name, DataFlow::Node res,
109+
DataFlow::StepSummary summary
110+
) {
111+
DataFlow::StepSummary::step(urls_attr(t2, attr_name), res, summary)
112+
}
113+
114+
/**
115+
* Gets a reference to the attribute `attr_name` of the `urls` module.
116+
* WARNING: Only holds for a few predefined attributes.
117+
*/
118+
private DataFlow::Node urls_attr(string attr_name) {
119+
result = urls_attr(DataFlow::TypeTracker::end(), attr_name)
120+
}
121+
122+
/**
123+
* Gets a reference to the `django.urls.path` function.
124+
* See https://docs.djangoproject.com/en/3.0/ref/urls/#path
125+
*/
126+
DataFlow::Node path() { result = urls_attr("path") }
127+
128+
/**
129+
* Gets a reference to the `django.urls.re_path` function.
130+
* See https://docs.djangoproject.com/en/3.0/ref/urls/#re_path
131+
*/
132+
DataFlow::Node re_path() { result = urls_attr("re_path") }
133+
}
134+
}
135+
136+
private class DjangoPathRouteSetup extends HTTP::Server::RouteSetup::Range, DataFlow::CfgNode {
137+
override CallNode node;
138+
139+
DjangoPathRouteSetup() { node.getFunction() = django::urls::path().asCfgNode() }
140+
141+
override string getUrlPattern() {
142+
exists(StrConst str, ControlFlowNode urlPatternArg |
143+
urlPatternArg = [node.getArg(0), node.getArgByName("route")]
144+
|
145+
DataFlow::localFlow(DataFlow::exprNode(str),
146+
any(DataFlow::Node n | n.asCfgNode() = urlPatternArg)) and
147+
result = str.getText()
148+
)
149+
}
150+
151+
override Function getARouteHandler() { none() }
152+
153+
override Parameter getARoutedParameter() { none() }
154+
}
155+
156+
private class DjangoRePathRouteSetup extends HTTP::Server::RouteSetup::Range, DataFlow::CfgNode {
157+
override CallNode node;
158+
159+
DjangoRePathRouteSetup() { node.getFunction() = django::urls::re_path().asCfgNode() }
160+
161+
override string getUrlPattern() {
162+
exists(StrConst str, ControlFlowNode urlPatternArg |
163+
urlPatternArg = [node.getArg(0), node.getArgByName("route")]
164+
|
165+
DataFlow::localFlow(DataFlow::exprNode(str),
166+
any(DataFlow::Node n | n.asCfgNode() = urlPatternArg)) and
167+
result = str.getText()
168+
)
169+
}
170+
171+
override Function getARouteHandler() { none() }
172+
173+
override Parameter getARoutedParameter() { none() }
174+
}
175+
}

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

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,9 @@
1313
| routing_test.py:44:62:44:120 | Comment # $routeHandler $routedParameter=arg0 $routedParameter=arg1 | Missing result:routeHandler= |
1414
| routing_test.py:44:62:44:120 | Comment # $routeHandler $routedParameter=arg0 $routedParameter=arg1 | Missing result:routedParameter=arg0 |
1515
| routing_test.py:44:62:44:120 | Comment # $routeHandler $routedParameter=arg0 $routedParameter=arg1 | Missing result:routedParameter=arg1 |
16-
| routing_test.py:49:75:49:131 | Comment # $routeSetup=r"^url_match/(?P<foo>[^/]+)/(?P<bar>[^/]+)" | Missing result:routeSetup=r"^url_match/(?P<foo>[^/]+)/(?P<bar>[^/]+)" |
17-
| routing_test.py:50:47:50:74 | Comment # $routeSetup=r"^get_params" | Missing result:routeSetup=r"^get_params" |
18-
| routing_test.py:51:49:51:77 | Comment # $routeSetup=r"^post_params" | Missing result:routeSetup=r"^post_params" |
19-
| routing_test.py:52:53:52:85 | Comment # $routeSetup=r"^http_resp_write" | Missing result:routeSetup=r"^http_resp_write" |
20-
| routing_test.py:53:70:53:115 | Comment # $routeSetup=r"^class_view/(?P<untrusted>.+)" | Missing result:routeSetup=r"^class_view/(?P<untrusted>.+)" |
21-
| routing_test.py:56:76:56:133 | Comment # $routeSetup=r"articles/^(?:page-(?P<page_number>\\d+)/)?" | Missing result:routeSetup=r"articles/^(?:page-(?P<page_number>\\d+)/)?" |
22-
| routing_test.py:59:95:59:139 | Comment # $routeSetup=r"^([^/]+)/(?:foo\|bar)/([^/]+)" | Missing result:routeSetup=r"^([^/]+)/(?:foo\|bar)/([^/]+)" |
2316
| routing_test.py:65:31:65:45 | Comment # $routeHandler | Missing result:routeHandler= |
24-
| routing_test.py:70:84:70:138 | Comment # $routeSetup=r"^specifying-as-kwargs-is-not-a-problem" | Missing result:routeSetup=r"^specifying-as-kwargs-is-not-a-problem" |
17+
| routing_test.py:70:5:70:81 | ControlFlowNode for re_path() | Unexpected result: routeSetup= |
18+
| routing_test.py:70:84:70:137 | Comment # $routeSetup="^specifying-as-kwargs-is-not-a-problem" | Missing result:routeSetup="^specifying-as-kwargs-is-not-a-problem" |
2519
| routing_test.py:78:43:78:86 | Comment # $routeHandler $routedParameter=page_number | Missing result:routeHandler= |
2620
| routing_test.py:78:43:78:86 | Comment # $routeHandler $routedParameter=page_number | Missing result:routedParameter=page_number |
2721
| routing_test.py:81:43:81:120 | Comment # $routeHandler $routedParameter=foo $routedParameter=bar $routedParameter=baz | Missing result:routeHandler= |
@@ -32,12 +26,5 @@
3226
| routing_test.py:84:38:84:94 | Comment # $routeHandler $routedParameter=foo $routedParameter=bar | Missing result:routedParameter=bar |
3327
| routing_test.py:84:38:84:94 | Comment # $routeHandler $routedParameter=foo $routedParameter=bar | Missing result:routedParameter=foo |
3428
| routing_test.py:87:37:87:51 | Comment # $routeHandler | Missing result:routeHandler= |
35-
| routing_test.py:91:38:91:62 | Comment # $routeSetup="articles/" | Missing result:routeSetup="articles/" |
36-
| routing_test.py:92:60:92:106 | Comment # $routeSetup="articles/page-<int:page_number>" | Missing result:routeSetup="articles/page-<int:page_number>" |
37-
| routing_test.py:93:74:93:114 | Comment # $routeSetup="<int:foo>/<str:bar>/<baz>" | Missing result:routeSetup="<int:foo>/<str:bar>/<baz>" |
38-
| routing_test.py:95:51:95:77 | Comment # $routeSetup="<foo>/<bar>" | Missing result:routeSetup="<foo>/<bar>" |
39-
| routing_test.py:98:60:98:97 | Comment # $routeSetup="not_valid/<not_valid!>" | Missing result:routeSetup="not_valid/<not_valid!>" |
40-
| testapp/urls.py:6:31:6:50 | Comment # $routeSetup="foo/" | Missing result:routeSetup="foo/" |
41-
| testapp/urls.py:10:43:10:67 | Comment # $routeSetup=r"^ba[rz]/" | Missing result:routeSetup=r"^ba[rz]/" |
4229
| testapp/views.py:3:33:3:47 | Comment # $routeHandler | Missing result:routeHandler= |
4330
| testapp/views.py:6:37:6:51 | Comment # $routeHandler | Missing result:routeHandler= |

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

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

55
from testproj import *
66
from testapp import *
7+
8+
import os.path as pth
9+
10+
pth.join("foo", "bar")

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,17 @@ def xxs_positional_arg(request, arg0, arg1, no_taint=None): # $routeHandler $ro
4646

4747

4848
urlpatterns = [
49-
re_path(r"^url_match/(?P<foo>[^/]+)/(?P<bar>[^/]+)", url_match_xss), # $routeSetup=r"^url_match/(?P<foo>[^/]+)/(?P<bar>[^/]+)"
50-
re_path(r"^get_params", get_params_xss), # $routeSetup=r"^get_params"
51-
re_path(r"^post_params", post_params_xss), # $routeSetup=r"^post_params"
52-
re_path(r"^http_resp_write", http_resp_write), # $routeSetup=r"^http_resp_write"
53-
re_path(r"^class_view/(?P<untrusted>.+)", ClassView.as_view()), # $routeSetup=r"^class_view/(?P<untrusted>.+)"
49+
re_path(r"^url_match/(?P<foo>[^/]+)/(?P<bar>[^/]+)", url_match_xss), # $routeSetup="^url_match/(?P<foo>[^/]+)/(?P<bar>[^/]+)"
50+
re_path(r"^get_params", get_params_xss), # $routeSetup="^get_params"
51+
re_path(r"^post_params", post_params_xss), # $routeSetup="^post_params"
52+
re_path(r"^http_resp_write", http_resp_write), # $routeSetup="^http_resp_write"
53+
re_path(r"^class_view/(?P<untrusted>.+)", ClassView.as_view()), # $routeSetup="^class_view/(?P<untrusted>.+)"
5454

5555
# one pattern to support `articles/page-<n>` and ensuring that articles/ goes to page-1
56-
re_path(r"articles/^(?:page-(?P<page_number>\d+)/)?", show_articles), # $routeSetup=r"articles/^(?:page-(?P<page_number>\d+)/)?"
56+
re_path(r"articles/^(?:page-(?P<page_number>\d+)/)?", show_articles), # $routeSetup="articles/^(?:page-(?P<page_number>\d+)/)?"
5757
# passing as positional argument is not the recommended way of doing things, but it is certainly
5858
# possible
59-
re_path(r"^([^/]+)/(?:foo|bar)/([^/]+)", xxs_positional_arg, name='xxs_positional_arg'), # $routeSetup=r"^([^/]+)/(?:foo|bar)/([^/]+)"
59+
re_path(r"^([^/]+)/(?:foo|bar)/([^/]+)", xxs_positional_arg, name='xxs_positional_arg'), # $routeSetup="^([^/]+)/(?:foo|bar)/([^/]+)"
6060
]
6161

6262

@@ -67,7 +67,7 @@ def re_path_kwargs(request): # $routeHandler
6767

6868

6969
urlpatterns = [
70-
re_path(view=re_path_kwargs, regex=r"^specifying-as-kwargs-is-not-a-problem") # $routeSetup=r"^specifying-as-kwargs-is-not-a-problem"
70+
re_path(view=re_path_kwargs, regex=r"^specifying-as-kwargs-is-not-a-problem") # $routeSetup="^specifying-as-kwargs-is-not-a-problem"
7171
]
7272

7373
################################################################################

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
# TODO: Doesn't include standard `$` to mark end of string, due to problems with
88
# inline expectation tests (which thinks the `$` would mark the beginning of a new
99
# line)
10-
re_path(r"^ba[rz]/", views.bar_baz), # $routeSetup=r"^ba[rz]/"
10+
re_path(r"^ba[rz]/", views.bar_baz), # $routeSetup="^ba[rz]/"
1111
]

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@
1717
from django.urls import path, include
1818

1919
urlpatterns = [
20-
path('admin/', admin.site.urls),
21-
path("app/", include("testapp.urls")),
20+
path("admin/", admin.site.urls), # $routeSetup="admin/"
21+
path("app/", include("testapp.urls")), # $routeSetup="app/"
2222
]

0 commit comments

Comments
 (0)