Skip to content

Commit bc56d16

Browse files
authored
Merge pull request github#5485 from RasmusWL/django-queryset-chains
Approved by tausbn
2 parents d35a501 + c8a6e83 commit bc56d16

File tree

3 files changed

+51
-73
lines changed

3 files changed

+51
-73
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 QuerySet chains such as `User.objects.using("db-name").exclude(username="admin").extra("some sql")`. This can lead to new results for `py/sql-injection`.

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

Lines changed: 46 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,6 @@ private module Django {
105105
// -------------------------------------------------------------------------
106106
// django.db.models
107107
// -------------------------------------------------------------------------
108-
// NOTE: The modelling of django models is currently fairly incomplete.
109-
// It does not fully take `Model`s, `Manager`s, `and QuerySet`s into account.
110-
// It simply identifies some common dangerous cases.
111108
/** Gets a reference to the `django.db.models` module. */
112109
private DataFlow::Node models(DataFlow::TypeTracker t) {
113110
t.start() and
@@ -124,71 +121,51 @@ private module Django {
124121

125122
/** Provides models for the `django.db.models` module. */
126123
module models {
127-
/** Provides models for the `django.db.models.Model` class. */
124+
/**
125+
* Provides models for the `django.db.models.Model` class and subclasses.
126+
*
127+
* See https://docs.djangoproject.com/en/3.1/topics/db/models/.
128+
*/
128129
module Model {
129-
/** Gets a reference to the `django.db.models.Model` class. */
130-
private DataFlow::Node classRef(DataFlow::TypeTracker t) {
131-
t.start() and
132-
result = DataFlow::importNode("django.db.models.Model")
133-
or
134-
t.startInAttr("Model") and
135-
result = models()
136-
or
137-
// subclass
138-
result.asExpr().(ClassExpr).getABase() = classRef(t.continue()).asExpr()
139-
or
140-
exists(DataFlow::TypeTracker t2 | result = classRef(t2).track(t2, t))
130+
/** Gets a reference to the `flask.views.View` class or any subclass. */
131+
API::Node subclassRef() {
132+
result =
133+
API::moduleImport("django")
134+
.getMember("db")
135+
.getMember("models")
136+
.getMember("Model")
137+
.getASubclass*()
141138
}
142-
143-
/** Gets a reference to the `django.db.models.Model` class. */
144-
DataFlow::Node classRef() { result = classRef(DataFlow::TypeTracker::end()) }
145-
}
146-
147-
/** Gets a reference to the `objects` object of a django model. */
148-
private DataFlow::Node objects(DataFlow::TypeTracker t) {
149-
t.startInAttr("objects") and
150-
result = Model::classRef()
151-
or
152-
exists(DataFlow::TypeTracker t2 | result = objects(t2).track(t2, t))
153139
}
154140

155-
/** Gets a reference to the `objects` object of a model. */
156-
DataFlow::Node objects() { result = objects(DataFlow::TypeTracker::end()) }
157-
158141
/**
159-
* Gets a reference to the attribute `attr_name` of an `objects` object.
160-
* WARNING: Only holds for a few predefined attributes.
142+
* Gets a reference to the Manager (django.db.models.Manager) for a django Model,
143+
* accessed by `<ModelName>.objects`.
161144
*/
162-
private DataFlow::Node objects_attr(DataFlow::TypeTracker t, string attr_name) {
163-
attr_name in ["annotate", "extra", "raw"] and
164-
t.startInAttr(attr_name) and
165-
result = objects()
166-
or
167-
// Due to bad performance when using normal setup with `objects_attr(t2, attr_name).track(t2, t)`
168-
// we have inlined that code and forced a join
169-
exists(DataFlow::TypeTracker t2 |
170-
exists(DataFlow::StepSummary summary |
171-
objects_attr_first_join(t2, attr_name, result, summary) and
172-
t = t2.append(summary)
173-
)
174-
)
175-
}
145+
API::Node manager() { result = Model::subclassRef().getMember("objects") }
176146

177-
pragma[nomagic]
178-
private predicate objects_attr_first_join(
179-
DataFlow::TypeTracker t2, string attr_name, DataFlow::Node res,
180-
DataFlow::StepSummary summary
181-
) {
182-
DataFlow::StepSummary::step(objects_attr(t2, attr_name), res, summary)
147+
/**
148+
* Gets a method with `name` that returns a QuerySet.
149+
* This method can originate on a QuerySet or a Manager.
150+
*
151+
* See https://docs.djangoproject.com/en/3.1/ref/models/querysets/
152+
*/
153+
API::Node querySetReturningMethod(string name) {
154+
name in [
155+
"none", "all", "filter", "exclude", "complex_filter", "union", "intersection",
156+
"difference", "select_for_update", "select_related", "prefetch_related", "order_by",
157+
"distinct", "reverse", "defer", "only", "using", "annotate", "extra", "raw",
158+
"datetimes", "dates", "values", "values_list"
159+
] and
160+
result = [manager(), querySet()].getMember(name)
183161
}
184162

185163
/**
186-
* Gets a reference to the attribute `attr_name` of an `objects` object.
187-
* WARNING: Only holds for a few predefined attributes.
164+
* Gets a reference to a QuerySet (django.db.models.query.QuerySet).
165+
*
166+
* See https://docs.djangoproject.com/en/3.1/ref/models/querysets/
188167
*/
189-
DataFlow::Node objects_attr(string attr_name) {
190-
result = objects_attr(DataFlow::TypeTracker::end(), attr_name)
191-
}
168+
API::Node querySet() { result = querySetReturningMethod(_).getReturn() }
192169

193170
/** Gets a reference to the `django.db.models.expressions` module. */
194171
private DataFlow::Node expressions(DataFlow::TypeTracker t) {
@@ -254,14 +231,13 @@ private module Django {
254231
*
255232
* See https://docs.djangoproject.com/en/3.1/ref/models/querysets/#annotate
256233
*/
257-
private class ObjectsAnnotate extends SqlExecution::Range, DataFlow::CfgNode {
258-
override CallNode node;
234+
private class ObjectsAnnotate extends SqlExecution::Range, DataFlow::CallCfgNode {
259235
ControlFlowNode sql;
260236

261237
ObjectsAnnotate() {
262-
node.getFunction() = django::db::models::objects_attr("annotate").asCfgNode() and
263-
django::db::models::expressions::RawSQL::instance(sql).asCfgNode() in [
264-
node.getArg(_), node.getArgByName(_)
238+
this = django::db::models::querySetReturningMethod("annotate").getACall() and
239+
django::db::models::expressions::RawSQL::instance(sql) in [
240+
this.getArg(_), this.getArgByName(_)
265241
]
266242
}
267243

@@ -275,27 +251,24 @@ private module Django {
275251
* - https://docs.djangoproject.com/en/3.1/topics/db/sql/#django.db.models.Manager.raw
276252
* - https://docs.djangoproject.com/en/3.1/ref/models/querysets/#raw
277253
*/
278-
private class ObjectsRaw extends SqlExecution::Range, DataFlow::CfgNode {
279-
override CallNode node;
280-
281-
ObjectsRaw() { node.getFunction() = django::db::models::objects_attr("raw").asCfgNode() }
254+
private class ObjectsRaw extends SqlExecution::Range, DataFlow::CallCfgNode {
255+
ObjectsRaw() { this = django::db::models::querySetReturningMethod("raw").getACall() }
282256

283-
override DataFlow::Node getSql() { result.asCfgNode() = node.getArg(0) }
257+
override DataFlow::Node getSql() { result = this.getArg(0) }
284258
}
285259

286260
/**
287261
* A call to the `extra` function on a model.
288262
*
289263
* See https://docs.djangoproject.com/en/3.1/ref/models/querysets/#extra
290264
*/
291-
private class ObjectsExtra extends SqlExecution::Range, DataFlow::CfgNode {
292-
override CallNode node;
293-
294-
ObjectsExtra() { node.getFunction() = django::db::models::objects_attr("extra").asCfgNode() }
265+
private class ObjectsExtra extends SqlExecution::Range, DataFlow::CallCfgNode {
266+
ObjectsExtra() { this = django::db::models::querySetReturningMethod("extra").getACall() }
295267

296268
override DataFlow::Node getSql() {
297-
result.asCfgNode() =
298-
[node.getArg([0, 1, 3, 4]), node.getArgByName(["select", "where", "tables", "order_by"])]
269+
result in [
270+
this.getArg([0, 1, 3, 4]), this.getArgByName(["select", "where", "tables", "order_by"])
271+
]
299272
}
300273
}
301274

python/ql/test/library-tests/frameworks/django/SqlExecution.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,6 @@ def test_model():
2727

2828
raw = RawSQL("so raw")
2929
User.objects.annotate(val=raw) # $getSql="so raw"
30+
31+
# chaining QuerySet calls
32+
User.objects.using("db-name").exclude(username="admin").extra("some sql") # $ getSql="some sql"

0 commit comments

Comments
 (0)