Skip to content

Commit c8a6e83

Browse files
committed
Python: Model QuerySet chains in django
1 parent 701b935 commit c8a6e83

File tree

3 files changed

+50
-74
lines changed

3 files changed

+50
-74
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: 47 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ private import semmle.python.dataflow.new.DataFlow
88
private import semmle.python.dataflow.new.RemoteFlowSources
99
private import semmle.python.dataflow.new.TaintTracking
1010
private import semmle.python.Concepts
11+
private import semmle.python.ApiGraphs
1112
private import semmle.python.frameworks.PEP249
1213
private import semmle.python.regex
1314

@@ -104,9 +105,6 @@ private module Django {
104105
// -------------------------------------------------------------------------
105106
// django.db.models
106107
// -------------------------------------------------------------------------
107-
// NOTE: The modelling of django models is currently fairly incomplete.
108-
// It does not fully take `Model`s, `Manager`s, `and QuerySet`s into account.
109-
// It simply identifies some common dangerous cases.
110108
/** Gets a reference to the `django.db.models` module. */
111109
private DataFlow::Node models(DataFlow::TypeTracker t) {
112110
t.start() and
@@ -123,71 +121,51 @@ private module Django {
123121

124122
/** Provides models for the `django.db.models` module. */
125123
module models {
126-
/** 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+
*/
127129
module Model {
128-
/** Gets a reference to the `django.db.models.Model` class. */
129-
private DataFlow::Node classRef(DataFlow::TypeTracker t) {
130-
t.start() and
131-
result = DataFlow::importNode("django.db.models.Model")
132-
or
133-
t.startInAttr("Model") and
134-
result = models()
135-
or
136-
// subclass
137-
result.asExpr().(ClassExpr).getABase() = classRef(t.continue()).asExpr()
138-
or
139-
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*()
140138
}
141-
142-
/** Gets a reference to the `django.db.models.Model` class. */
143-
DataFlow::Node classRef() { result = classRef(DataFlow::TypeTracker::end()) }
144-
}
145-
146-
/** Gets a reference to the `objects` object of a django model. */
147-
private DataFlow::Node objects(DataFlow::TypeTracker t) {
148-
t.startInAttr("objects") and
149-
result = Model::classRef()
150-
or
151-
exists(DataFlow::TypeTracker t2 | result = objects(t2).track(t2, t))
152139
}
153140

154-
/** Gets a reference to the `objects` object of a model. */
155-
DataFlow::Node objects() { result = objects(DataFlow::TypeTracker::end()) }
156-
157141
/**
158-
* Gets a reference to the attribute `attr_name` of an `objects` object.
159-
* 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`.
160144
*/
161-
private DataFlow::Node objects_attr(DataFlow::TypeTracker t, string attr_name) {
162-
attr_name in ["annotate", "extra", "raw"] and
163-
t.startInAttr(attr_name) and
164-
result = objects()
165-
or
166-
// Due to bad performance when using normal setup with `objects_attr(t2, attr_name).track(t2, t)`
167-
// we have inlined that code and forced a join
168-
exists(DataFlow::TypeTracker t2 |
169-
exists(DataFlow::StepSummary summary |
170-
objects_attr_first_join(t2, attr_name, result, summary) and
171-
t = t2.append(summary)
172-
)
173-
)
174-
}
145+
API::Node manager() { result = Model::subclassRef().getMember("objects") }
175146

176-
pragma[nomagic]
177-
private predicate objects_attr_first_join(
178-
DataFlow::TypeTracker t2, string attr_name, DataFlow::Node res,
179-
DataFlow::StepSummary summary
180-
) {
181-
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)
182161
}
183162

184163
/**
185-
* Gets a reference to the attribute `attr_name` of an `objects` object.
186-
* 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/
187167
*/
188-
DataFlow::Node objects_attr(string attr_name) {
189-
result = objects_attr(DataFlow::TypeTracker::end(), attr_name)
190-
}
168+
API::Node querySet() { result = querySetReturningMethod(_).getReturn() }
191169

192170
/** Gets a reference to the `django.db.models.expressions` module. */
193171
private DataFlow::Node expressions(DataFlow::TypeTracker t) {
@@ -253,14 +231,13 @@ private module Django {
253231
*
254232
* See https://docs.djangoproject.com/en/3.1/ref/models/querysets/#annotate
255233
*/
256-
private class ObjectsAnnotate extends SqlExecution::Range, DataFlow::CfgNode {
257-
override CallNode node;
234+
private class ObjectsAnnotate extends SqlExecution::Range, DataFlow::CallCfgNode {
258235
ControlFlowNode sql;
259236

260237
ObjectsAnnotate() {
261-
node.getFunction() = django::db::models::objects_attr("annotate").asCfgNode() and
262-
django::db::models::expressions::RawSQL::instance(sql).asCfgNode() in [
263-
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(_)
264241
]
265242
}
266243

@@ -274,27 +251,24 @@ private module Django {
274251
* - https://docs.djangoproject.com/en/3.1/topics/db/sql/#django.db.models.Manager.raw
275252
* - https://docs.djangoproject.com/en/3.1/ref/models/querysets/#raw
276253
*/
277-
private class ObjectsRaw extends SqlExecution::Range, DataFlow::CfgNode {
278-
override CallNode node;
279-
280-
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() }
281256

282-
override DataFlow::Node getSql() { result.asCfgNode() = node.getArg(0) }
257+
override DataFlow::Node getSql() { result = this.getArg(0) }
283258
}
284259

285260
/**
286261
* A call to the `extra` function on a model.
287262
*
288263
* See https://docs.djangoproject.com/en/3.1/ref/models/querysets/#extra
289264
*/
290-
private class ObjectsExtra extends SqlExecution::Range, DataFlow::CfgNode {
291-
override CallNode node;
292-
293-
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() }
294267

295268
override DataFlow::Node getSql() {
296-
result.asCfgNode() =
297-
[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+
]
298272
}
299273
}
300274

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,4 @@ def test_model():
2929
User.objects.annotate(val=raw) # $getSql="so raw"
3030

3131
# chaining QuerySet calls
32-
User.objects.using("db-name").exclude(username="admin").extra("some sql") # $ MISSING: getSql="some sql"
32+
User.objects.using("db-name").exclude(username="admin").extra("some sql") # $ getSql="some sql"

0 commit comments

Comments
 (0)