Skip to content

Commit 62d3063

Browse files
committed
Python: Add rest_framework Request taint modeling
1 parent 5d77e62 commit 62d3063

File tree

3 files changed

+105
-18
lines changed

3 files changed

+105
-18
lines changed

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,52 @@ module Django {
369369
}
370370
}
371371

372+
/**
373+
* Provides models for the `django.contrib.auth.models.User` class
374+
*
375+
* See https://docs.djangoproject.com/en/3.2/ref/contrib/auth/#user-model.
376+
*/
377+
module User {
378+
/**
379+
* A source of instances of `django.contrib.auth.models.User`, extend this class to model new instances.
380+
*
381+
* This can include instantiations of the class, return values from function
382+
* calls, or a special parameter that will be set when functions are called by an external
383+
* library.
384+
*
385+
* Use the predicate `User::instance()` to get references to instances of `django.contrib.auth.models.User`.
386+
*/
387+
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
388+
389+
/** Gets a reference to an instance of `django.contrib.auth.models.User`. */
390+
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
391+
t.start() and
392+
result instanceof InstanceSource
393+
or
394+
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
395+
}
396+
397+
/** Gets a reference to an instance of `django.contrib.auth.models.User`. */
398+
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
399+
400+
/**
401+
* Taint propagation for `django.contrib.auth.models.User`.
402+
*/
403+
private class InstanceTaintSteps extends InstanceTaintStepsHelper {
404+
InstanceTaintSteps() { this = "django.contrib.auth.models.User" }
405+
406+
override DataFlow::Node getInstance() { result = instance() }
407+
408+
override string getAttributeName() {
409+
result in ["username", "first_name", "last_name", "email"]
410+
}
411+
412+
override string getMethodName() { none() }
413+
414+
override string getAsyncMethodName() { none() }
415+
}
416+
}
417+
372418
/**
373419
* Provides models for the `django.core.files.uploadedfile.UploadedFile` class
374420
*

python/ql/lib/semmle/python/frameworks/RestFramework.qll

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,5 +171,46 @@ private module RestFramework {
171171

172172
/** Gets a reference to an instance of `rest_framework.request.Request`. */
173173
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
174+
175+
/**
176+
* Taint propagation for `rest_framework.request.Request`.
177+
*/
178+
private class InstanceTaintSteps extends InstanceTaintStepsHelper {
179+
InstanceTaintSteps() { this = "rest_framework.request.Request" }
180+
181+
override DataFlow::Node getInstance() { result = instance() }
182+
183+
override string getAttributeName() {
184+
result in ["data", "query_params", "user", "auth", "content_type", "stream"]
185+
}
186+
187+
override string getMethodName() { none() }
188+
189+
override string getAsyncMethodName() { none() }
190+
}
191+
192+
/** An attribute read that is a `MultiValueDict` instance. */
193+
private class MultiValueDictInstances extends Django::MultiValueDict::InstanceSource {
194+
MultiValueDictInstances() {
195+
this.(DataFlow::AttrRead).getObject() = instance() and
196+
this.(DataFlow::AttrRead).getAttributeName() = "query_params"
197+
}
198+
}
199+
200+
/** An attribute read that is a `User` instance. */
201+
private class UserInstances extends Django::User::InstanceSource {
202+
UserInstances() {
203+
this.(DataFlow::AttrRead).getObject() = instance() and
204+
this.(DataFlow::AttrRead).getAttributeName() = "user"
205+
}
206+
}
207+
208+
/** An attribute read that is a file-like instance. */
209+
private class FileLikeInstances extends Stdlib::FileLikeObject::InstanceSource {
210+
FileLikeInstances() {
211+
this.(DataFlow::AttrRead).getObject() = instance() and
212+
this.(DataFlow::AttrRead).getAttributeName() = "stream"
213+
}
214+
}
174215
}
175216
}

python/ql/test/library-tests/frameworks/rest_framework/taint_test.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,29 +25,29 @@ def test_taint(request: Request, routed_param): # $ requestHandler routedParamet
2525

2626
# special new attributes added, see https://www.django-rest-framework.org/api-guide/requests/
2727
ensure_tainted(
28-
request.data, # $ MISSING: tainted
29-
request.data["key"], # $ MISSING: tainted
28+
request.data, # $ tainted
29+
request.data["key"], # $ tainted
3030

3131
# alias for .GET
32-
request.query_params, # $ MISSING: tainted
33-
request.query_params["key"], # $ MISSING: tainted
34-
request.query_params.get("key"), # $ MISSING: tainted
35-
request.query_params.getlist("key"), # $ MISSING: tainted
36-
request.query_params.getlist("key")[0], # $ MISSING: tainted
37-
request.query_params.pop("key"), # $ MISSING: tainted
38-
request.query_params.pop("key")[0], # $ MISSING: tainted
32+
request.query_params, # $ tainted
33+
request.query_params["key"], # $ tainted
34+
request.query_params.get("key"), # $ tainted
35+
request.query_params.getlist("key"), # $ tainted
36+
request.query_params.getlist("key")[0], # $ tainted
37+
request.query_params.pop("key"), # $ tainted
38+
request.query_params.pop("key")[0], # $ tainted
3939

4040
# see more detailed tests of `request.user` below
41-
request.user, # $ MISSING: tainted
41+
request.user, # $ tainted
4242

43-
request.auth, # $ MISSING: tainted
43+
request.auth, # $ tainted
4444

4545
# seems much more likely attack vector than .method, so included
4646
request.content_type, # $ tainted
4747

4848
# file-like
49-
request.stream, # $ MISSING: tainted
50-
request.stream.read(), # $ MISSING: tainted
49+
request.stream, # $ tainted
50+
request.stream.read(), # $ tainted
5151
)
5252

5353
ensure_not_tainted(
@@ -74,10 +74,10 @@ def test_taint(request: Request, routed_param): # $ requestHandler routedParamet
7474
# username/email is user-controlled, but that password isn't (since it's a hash).
7575
# see https://docs.djangoproject.com/en/3.2/ref/contrib/auth/#fields
7676
ensure_tainted(
77-
request.user.username, # $ MISSING: tainted
78-
request.user.first_name, # $ MISSING: tainted
79-
request.user.last_name, # $ MISSING: tainted
80-
request.user.email, # $ MISSING: tainted
77+
request.user.username, # $ tainted
78+
request.user.first_name, # $ tainted
79+
request.user.last_name, # $ tainted
80+
request.user.email, # $ tainted
8181
)
8282
ensure_not_tainted(request.user.password)
8383

@@ -99,7 +99,7 @@ def get(self, request: Request, routed_param): # $ requestHandler routedParamete
9999
# request taint is the same as in function_based_view above
100100
ensure_tainted(
101101
request, # $ tainted
102-
request.data # $ MISSING: tainted
102+
request.data # $ tainted
103103
)
104104

105105
# same as for standard Django view

0 commit comments

Comments
 (0)