Skip to content

Commit fea46b6

Browse files
committed
Python: ORM: Handle <Model>.objects.create and friends
1 parent 9b458b5 commit fea46b6

File tree

2 files changed

+33
-4
lines changed

2 files changed

+33
-4
lines changed

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -836,6 +836,35 @@ module PrivateDjango {
836836
nodeTo = call
837837
)
838838
or
839+
// attribute store in `<Model>.objects.create`, `get_or_create`, and `update_or_create`
840+
// see https://docs.djangoproject.com/en/4.0/ref/models/querysets/#create
841+
// see https://docs.djangoproject.com/en/4.0/ref/models/querysets/#get-or-create
842+
// see https://docs.djangoproject.com/en/4.0/ref/models/querysets/#update-or-create
843+
// TODO: This does currently not handle values passed in the `defaults` dictionary
844+
exists(
845+
DataFlow::CallCfgNode call, API::Node modelClass, string fieldName,
846+
string methodName
847+
|
848+
modelClass = Model::subclassRef() and
849+
methodName in ["create", "get_or_create", "update_or_create"] and
850+
call = modelClass.getMember("objects").getMember(methodName).getACall() and
851+
nodeFrom = call.getArgByName(fieldName) and
852+
c.(DataFlow::AttributeContent).getAttribute() = fieldName and
853+
(
854+
// -> object created
855+
(
856+
methodName = "create" and nodeTo = call
857+
or
858+
// TODO: for these two methods, the result is a tuple `(<Model>, bool)`,
859+
// which we need flow-summaries to model properly
860+
methodName in ["get_or_create", "update_or_create"] and none()
861+
)
862+
or
863+
// -> DB store on synthetic node
864+
nodeTo.(SyntheticDjangoOrmModelNode).getModelClass() = modelClass
865+
)
866+
)
867+
or
839868
// synthetic -> method-call that returns collection of ORM models (all/filter/...)
840869
exists(API::Node modelClass |
841870
nodeFrom.(SyntheticDjangoOrmModelNode).getModelClass() = modelClass and

python/ql/test/library-tests/frameworks/django-orm/testapp/orm_tests.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,11 @@ class TestSave5(models.Model):
113113
def test_save5_store():
114114
# note: positional args not possible
115115
obj = TestSave5.objects.create(text=SOURCE)
116-
SINK(obj.text) # $ MISSING: flow
116+
SINK(obj.text) # $ flow="SOURCE, l:-1 -> obj.text"
117117

118118
def test_save5_load():
119119
obj = TestSave5.objects.first()
120-
SINK(obj.text) # $ MISSING: flow
120+
SINK(obj.text) # $ flow="SOURCE, l:-5 -> obj.text"
121121

122122
# --------------------------------------
123123
# <Model>.objects.get_or_create()
@@ -135,7 +135,7 @@ def test_save6_store():
135135
def test_save6_load():
136136
obj = TestSave6.objects.first()
137137
SINK(obj.text) # $ MISSING: flow
138-
SINK(obj.email) # $ MISSING: flow
138+
SINK(obj.email) # $ flow="SOURCE, l:-7 -> obj.email"
139139

140140
# --------------------------------------
141141
# <Model>.objects.update_or_create()
@@ -153,7 +153,7 @@ def test_save7_store():
153153
def test_save7_load():
154154
obj = TestSave7.objects.first()
155155
SINK(obj.text) # $ MISSING: flow
156-
SINK(obj.email) # $ MISSING: flow
156+
SINK(obj.email) # $ flow="SOURCE, l:-7 -> obj.email"
157157

158158
# --------------------------------------
159159
# <Model>.objects.[<QuerySet>].update()

0 commit comments

Comments
 (0)