Skip to content

Commit 5ec5557

Browse files
committed
Python: Model MultiValueDict in Django
1 parent 95e88c1 commit 5ec5557

File tree

2 files changed

+90
-11
lines changed

2 files changed

+90
-11
lines changed

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

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,78 @@ private module Django {
289289
API::Node subclassRef() { result = any(ModeledSubclass subclass).getASubclass*() }
290290
}
291291
}
292+
293+
/**
294+
* Provides models for the `django.utils.datastructures.MultiValueDict` class
295+
*
296+
* See
297+
* - https://docs.djangoproject.com/en/3.0/ref/request-response/#django.http.QueryDict (subclass that has proper docs)
298+
* - https://www.kite.com/python/docs/django.utils.datastructures.MultiValueDict
299+
*/
300+
module MultiValueDict {
301+
/** Gets a reference to the `django.utils.datastructures.MultiValueDict` class. */
302+
private API::Node classRef() {
303+
result =
304+
API::moduleImport("django")
305+
.getMember("utils")
306+
.getMember("datastructures")
307+
.getMember("MultiValueDict")
308+
}
309+
310+
/**
311+
* A source of instances of `django.utils.datastructures.MultiValueDict`, extend this class to model new instances.
312+
*
313+
* This can include instantiations of the class, return values from function
314+
* calls, or a special parameter that will be set when functions are called by an external
315+
* library.
316+
*
317+
* Use the predicate `MultiValueDict::instance()` to get references to instances of `django.utils.datastructures.MultiValueDict`.
318+
*/
319+
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
320+
321+
/** A direct instantiation of `django.utils.datastructures.MultiValueDict`. */
322+
private class ClassInstantiation extends InstanceSource, DataFlow::CallCfgNode {
323+
override CallNode node;
324+
325+
ClassInstantiation() { this = classRef().getACall() }
326+
}
327+
328+
/** Gets a reference to an instance of `django.utils.datastructures.MultiValueDict`. */
329+
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
330+
t.start() and
331+
result instanceof InstanceSource
332+
or
333+
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
334+
}
335+
336+
/** Gets a reference to an instance of `django.utils.datastructures.MultiValueDict`. */
337+
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
338+
339+
/**
340+
* Taint propagation for `django.utils.datastructures.MultiValueDict`.
341+
*/
342+
class MultiValueDictAdditionalTaintStep extends TaintTracking::AdditionalTaintStep {
343+
override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
344+
// class instantiation
345+
exists(ClassInstantiation call |
346+
nodeFrom = call.getArg(0) and
347+
nodeTo = call
348+
)
349+
or
350+
// Methods
351+
//
352+
// TODO: When we have tools that make it easy, model these properly to handle
353+
// `meth = obj.meth; meth()`. Until then, we'll use this more syntactic approach
354+
// (since it allows us to at least capture the most common cases).
355+
nodeFrom = instance() and
356+
exists(DataFlow::AttrRead attr | attr.getObject() = nodeFrom |
357+
// methods (non-async)
358+
attr.getAttributeName() in ["getlist", "lists", "popitem", "dict", "urlencode"] and
359+
nodeTo.(DataFlow::CallCfgNode).getFunction() = attr
360+
)
361+
}
362+
}
363+
}
292364
}
293365

294366
/**
@@ -1922,7 +1994,6 @@ private module PrivateDjango {
19221994
// str / bytes
19231995
"body", "path", "path_info", "method", "encoding", "content_type",
19241996
// django.http.QueryDict
1925-
// TODO: Model QueryDict
19261997
"GET", "POST",
19271998
// dict[str, str]
19281999
"content_params", "COOKIES",
@@ -1931,7 +2002,6 @@ private module PrivateDjango {
19312002
// HttpHeaders (case insensitive dict-like)
19322003
"headers",
19332004
// MultiValueDict[str, UploadedFile]
1934-
// TODO: Model MultiValueDict
19352005
// TODO: Model UploadedFile
19362006
"FILES",
19372007
// django.urls.ResolverMatch
@@ -1942,6 +2012,14 @@ private module PrivateDjango {
19422012
}
19432013
}
19442014

2015+
/** An attribute read on an django request that is a `MultiValueDict` instance. */
2016+
class DjangoHttpRequestMultiValueDictInstances extends Django::MultiValueDict::InstanceSource {
2017+
DjangoHttpRequestMultiValueDictInstances() {
2018+
this.(DataFlow::AttrRead).getObject() = django::http::request::HttpRequest::instance() and
2019+
this.(DataFlow::AttrRead).getAttributeName() in ["GET", "POST", "FILES"]
2020+
}
2021+
}
2022+
19452023
// ---------------------------------------------------------------------------
19462024
// django.shortcuts.redirect
19472025
// ---------------------------------------------------------------------------

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

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ def test_taint(request: HttpRequest, foo, bar, baz=None): # $requestHandler rou
3535
request.GET, # $ tainted
3636
request.GET["key"], # $ tainted
3737
request.GET.get("key"), # $ tainted
38-
request.GET.getlist("key"), # $ MISSING: tainted
39-
request.GET.getlist("key")[0], # $ MISSING: tainted
38+
request.GET.getlist("key"), # $ tainted
39+
request.GET.getlist("key")[0], # $ tainted
4040
request.GET.pop("key"), # $ tainted
4141
request.GET.pop("key")[0], # $ tainted
4242
# key
@@ -45,9 +45,10 @@ def test_taint(request: HttpRequest, foo, bar, baz=None): # $requestHandler rou
4545
request.GET.popitem()[1], # $ tainted
4646
# values[0]
4747
request.GET.popitem()[1][0], # $ tainted
48-
request.GET.dict(), # $ MISSING: tainted
49-
request.GET.dict()["key"], # $ MISSING: tainted
50-
request.GET.urlencode(), # $ MISSING: tainted
48+
request.GET.lists(), # $ tainted
49+
request.GET.dict(), # $ tainted
50+
request.GET.dict()["key"], # $ tainted
51+
request.GET.urlencode(), # $ tainted
5152

5253
# django.http.QueryDict (same as above, did not duplicate tests)
5354
request.POST, # $ tainted
@@ -70,11 +71,11 @@ def test_taint(request: HttpRequest, foo, bar, baz=None): # $requestHandler rou
7071

7172
request.FILES.get("key"), # $ tainted
7273
request.FILES.get("key").name, # $ MISSING: tainted
73-
request.FILES.getlist("key"), # $ MISSING: tainted
74-
request.FILES.getlist("key")[0], # $ MISSING: tainted
74+
request.FILES.getlist("key"), # $ tainted
75+
request.FILES.getlist("key")[0], # $ tainted
7576
request.FILES.getlist("key")[0].name, # $ MISSING: tainted
76-
request.FILES.dict(), # $ MISSING: tainted
77-
request.FILES.dict()["key"], # $ MISSING: tainted
77+
request.FILES.dict(), # $ tainted
78+
request.FILES.dict()["key"], # $ tainted
7879
request.FILES.dict()["key"].name, # $ MISSING: tainted
7980

8081
# Dict[str, Any]

0 commit comments

Comments
 (0)