Skip to content

Commit 48fba87

Browse files
committed
Python: ORM: add flow to base-class
1 parent 6b9dd49 commit 48fba87

File tree

2 files changed

+38
-7
lines changed

2 files changed

+38
-7
lines changed

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

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,32 @@ module PrivateDjango {
826826
API::Node getModelClass() { result = modelClass }
827827
}
828828

829+
/**
830+
* Gets a synthetic node where the data in the attribute `fieldName` can flow
831+
* to, when a DB store is made on `subModel`, taking ORM inheritance into
832+
* account.
833+
*
834+
* If `fieldName` is defined in class `base`, the results will include the
835+
* synthetic node for `base` itself, the synthetic node for `subModel`, as
836+
* well as all the classes in-between (in the class hierarchy).
837+
*/
838+
SyntheticDjangoOrmModelNode nodeToStoreIn(API::Node subModel, string fieldName) {
839+
exists(Class base, API::Node baseModel, API::Node resultModel |
840+
baseModel = Model::subclassRef() and
841+
resultModel = Model::subclassRef() and
842+
baseModel.getASubclass*() = subModel and
843+
base = getModelClassClass(baseModel) and
844+
exists(Variable v |
845+
base.getBody().getAnItem().(AssignStmt).defines(v) and
846+
v.getId() = fieldName
847+
)
848+
|
849+
baseModel.getASubclass*() = resultModel and
850+
resultModel.getASubclass*() = subModel and
851+
result.getModelClass() = resultModel
852+
)
853+
}
854+
829855
/** Additional data-flow steps for Django ORM models. */
830856
class DjangOrmSteps extends AdditionalOrmSteps {
831857
override predicate storeStep(
@@ -867,7 +893,7 @@ module PrivateDjango {
867893
)
868894
or
869895
// -> DB store on synthetic node
870-
nodeTo.(SyntheticDjangoOrmModelNode).getModelClass() = modelClass
896+
nodeTo = nodeToStoreIn(modelClass, fieldName)
871897
)
872898
)
873899
or
@@ -877,7 +903,7 @@ module PrivateDjango {
877903
call = [manager(modelClass), querySet(modelClass)].getMember("update").getACall() and
878904
nodeFrom = call.getArgByName(fieldName) and
879905
c.(DataFlow::AttributeContent).getAttribute() = fieldName and
880-
nodeTo.(SyntheticDjangoOrmModelNode).getModelClass() = modelClass
906+
nodeTo = nodeToStoreIn(modelClass, fieldName)
881907
)
882908
or
883909
// synthetic -> method-call that returns collection of ORM models (all/filter/...)
@@ -900,7 +926,12 @@ module PrivateDjango {
900926
override predicate jumpStep(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
901927
// save -> synthetic
902928
exists(API::Node modelClass, DataFlow::MethodCallNode saveCall |
903-
nodeTo.(SyntheticDjangoOrmModelNode).getModelClass() = modelClass and
929+
// TODO: The `nodeTo` should be restricted more, such that flow to
930+
// base-classes are only for the fields that are defined in the
931+
// base-class... but only passing on flow for a specific attribute requires flow-summaries,
932+
// so we can do
933+
// `obj (in obj.save call) ==read of attr==> synthetic attr on base-class ==store of attr==> synthetic for base-class`
934+
nodeTo = nodeToStoreIn(modelClass, _) and
904935
saveCall.calls(Model::instance(modelClass), "save") and
905936
nodeFrom = saveCall.getObject()
906937
)

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def fetch_book(id):
5252
try:
5353
# This sink should have 2 sources, from `save_base_book` and
5454
# `save_physical_book`
55-
SINK(book.title) # $ flow="SOURCE, l:-10 -> book.title"
55+
SINK(book.title) # $ flow="SOURCE, l:-10 -> book.title" flow="SOURCE, l:+21 -> book.title"
5656
# The sink assertion will fail for the EBook, which we handle. The title attribute
5757
# of a Book could be tainted, so we want this to be a sink in general.
5858
except AssertionError:
@@ -140,7 +140,7 @@ def poly_fetch_book(id, test_for_subclass=True):
140140
try:
141141
# This sink should have 2 sources, from `poly_save_base_book` and
142142
# `poly_save_physical_book`
143-
SINK(book.title) # $ flow="SOURCE, l:-10 -> book.title"
143+
SINK(book.title) # $ flow="SOURCE, l:-10 -> book.title" flow="SOURCE, l:+24 -> book.title"
144144
# The sink assertion will fail for the PolyEBook, which we handle. The title
145145
# attribute of a PolyBook could be tainted, so we want this to be a sink in general.
146146
except AssertionError:
@@ -153,11 +153,11 @@ def poly_fetch_book(id, test_for_subclass=True):
153153
assert isinstance(book, PolyPhysicalBook) or isinstance(book, PolyEBook)
154154

155155
if isinstance(book, PolyPhysicalBook):
156-
SINK(book.title) # $ MISSING: flow="SOURCE, l:+11 -> book.title" SPURIOUS: flow="SOURCE, l:-23 -> book.title"
156+
SINK(book.title) # $ flow="SOURCE, l:+11 -> book.title" SPURIOUS: flow="SOURCE, l:-23 -> book.title"
157157
SINK(book.physical_location) # $ MISSING: flow
158158
SINK(book.same_name_different_value) # $ MISSING: flow
159159
elif isinstance(book, PolyEBook):
160-
SINK_F(book.title) # $ SPURIOUS: flow="SOURCE, l:-27 -> book.title"
160+
SINK_F(book.title) # $ SPURIOUS: flow="SOURCE, l:-27 -> book.title" flow="SOURCE, l:+7 -> book.title"
161161
SINK_F(book.download_link)
162162
SINK_F(book.same_name_different_value)
163163

0 commit comments

Comments
 (0)