Skip to content

Commit 208d515

Browse files
authored
Merge pull request github#5500 from RasmusWL/django-forms
Python: Model RemoteFlowSources on Django forms/fields
2 parents 716e0f1 + 70974ea commit 208d515

File tree

4 files changed

+350
-24
lines changed

4 files changed

+350
-24
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 sources of remote user input (`RemoteFlowSource`) in Django forms (`django.forms.Form`) and fields (`django.forms.Field`) subclasses.

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

Lines changed: 291 additions & 24 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

@@ -1975,6 +1976,205 @@ private module Django {
19751976
}
19761977
}
19771978

1979+
/** Provides models for django forms (defined in the `django.forms` module) */
1980+
module Forms {
1981+
/**
1982+
* Provides models for the `django.forms.forms.BaseForm` class and subclasses. This
1983+
* is usually used by the `django.forms.forms.Form` class, which is also available
1984+
* under the more commonly used alias `django.forms.Form`.
1985+
*
1986+
* See https://docs.djangoproject.com/en/3.1/ref/forms/api/
1987+
*/
1988+
module Form {
1989+
/** Gets a reference to the `django.forms.forms.BaseForm` class or any subclass. */
1990+
API::Node subclassRef() {
1991+
// canonical definition
1992+
result =
1993+
API::moduleImport("django")
1994+
.getMember("forms")
1995+
.getMember("forms")
1996+
.getMember(["BaseForm", "Form"])
1997+
.getASubclass*()
1998+
or
1999+
result =
2000+
API::moduleImport("django")
2001+
.getMember("forms")
2002+
.getMember("models")
2003+
.getMember(["BaseModelForm", "ModelForm"])
2004+
.getASubclass*()
2005+
or
2006+
// aliases from `django.forms`
2007+
result =
2008+
API::moduleImport("django")
2009+
.getMember("forms")
2010+
.getMember(["BaseForm", "Form", "BaseModelForm", "ModelForm"])
2011+
.getASubclass*()
2012+
or
2013+
// other Form subclasses defined in Django
2014+
result =
2015+
API::moduleImport("django")
2016+
.getMember("contrib")
2017+
.getMember("admin")
2018+
.getMember("forms")
2019+
.getMember(["AdminAuthenticationForm", "AdminPasswordChangeForm"])
2020+
.getASubclass*()
2021+
or
2022+
result =
2023+
API::moduleImport("django")
2024+
.getMember("contrib")
2025+
.getMember("admin")
2026+
.getMember("helpers")
2027+
.getMember("ActionForm")
2028+
.getASubclass*()
2029+
or
2030+
result =
2031+
API::moduleImport("django")
2032+
.getMember("contrib")
2033+
.getMember("admin")
2034+
.getMember("views")
2035+
.getMember("main")
2036+
.getMember("ChangeListSearchForm")
2037+
.getASubclass*()
2038+
or
2039+
result =
2040+
API::moduleImport("django")
2041+
.getMember("contrib")
2042+
.getMember("auth")
2043+
.getMember("forms")
2044+
.getMember([
2045+
"PasswordResetForm", "UserChangeForm", "SetPasswordForm",
2046+
"AdminPasswordChangeForm", "PasswordChangeForm", "AuthenticationForm",
2047+
"UserCreationForm"
2048+
])
2049+
.getASubclass*()
2050+
or
2051+
result =
2052+
API::moduleImport("django")
2053+
.getMember("contrib")
2054+
.getMember("flatpages")
2055+
.getMember("forms")
2056+
.getMember("FlatpageForm")
2057+
.getASubclass*()
2058+
or
2059+
result =
2060+
API::moduleImport("django")
2061+
.getMember("forms")
2062+
.getMember("formsets")
2063+
.getMember("ManagementForm")
2064+
.getASubclass*()
2065+
or
2066+
result =
2067+
API::moduleImport("django")
2068+
.getMember("forms")
2069+
.getMember("models")
2070+
.getMember(["ModelForm", "BaseModelForm"])
2071+
.getASubclass*()
2072+
}
2073+
}
2074+
2075+
/**
2076+
* Provides models for the `django.forms.fields.Field` class and subclasses. This is
2077+
* also available under the more commonly used alias `django.forms.Field`.
2078+
*
2079+
* See https://docs.djangoproject.com/en/3.1/ref/forms/fields/
2080+
*/
2081+
module Field {
2082+
/** Gets a reference to the `django.forms.fields.Field` class or any subclass. */
2083+
API::Node subclassRef() {
2084+
exists(string modName, string clsName |
2085+
// canonical definition
2086+
result =
2087+
API::moduleImport("django")
2088+
.getMember("forms")
2089+
.getMember(modName)
2090+
.getMember(clsName)
2091+
.getASubclass*()
2092+
or
2093+
// alias from `django.forms`
2094+
result = API::moduleImport("django").getMember("forms").getMember(clsName).getASubclass*()
2095+
|
2096+
modName = "fields" and
2097+
clsName in [
2098+
"Field",
2099+
// Known subclasses
2100+
"BooleanField", "IntegerField", "CharField", "SlugField", "DateTimeField",
2101+
"EmailField", "DateField", "TimeField", "DurationField", "DecimalField", "FloatField",
2102+
"GenericIPAddressField", "UUIDField", "JSONField", "FilePathField",
2103+
"NullBooleanField", "URLField", "TypedChoiceField", "FileField", "ImageField",
2104+
"RegexField", "ChoiceField", "MultipleChoiceField", "ComboField", "MultiValueField",
2105+
"SplitDateTimeField", "TypedMultipleChoiceField", "BaseTemporalField"
2106+
]
2107+
or
2108+
// Known subclasses from `django.forms.models`
2109+
modName = "models" and
2110+
clsName in ["ModelChoiceField", "ModelMultipleChoiceField", "InlineForeignKeyField"]
2111+
)
2112+
or
2113+
// other Field subclasses defined in Django
2114+
result =
2115+
API::moduleImport("django")
2116+
.getMember("contrib")
2117+
.getMember("auth")
2118+
.getMember("forms")
2119+
.getMember(["ReadOnlyPasswordHashField", "UsernameField"])
2120+
.getASubclass*()
2121+
or
2122+
result =
2123+
API::moduleImport("django")
2124+
.getMember("contrib")
2125+
.getMember("gis")
2126+
.getMember("forms")
2127+
.getMember("fields")
2128+
.getMember([
2129+
"GeometryCollectionField", "GeometryField", "LineStringField",
2130+
"MultiLineStringField", "MultiPointField", "MultiPolygonField", "PointField",
2131+
"PolygonField"
2132+
])
2133+
.getASubclass*()
2134+
or
2135+
result =
2136+
API::moduleImport("django")
2137+
.getMember("contrib")
2138+
.getMember("postgres")
2139+
.getMember("forms")
2140+
.getMember("array")
2141+
.getMember(["SimpleArrayField", "SplitArrayField"])
2142+
.getASubclass*()
2143+
or
2144+
result =
2145+
API::moduleImport("django")
2146+
.getMember("contrib")
2147+
.getMember("postgres")
2148+
.getMember("forms")
2149+
.getMember("hstore")
2150+
.getMember("HStoreField")
2151+
.getASubclass*()
2152+
or
2153+
result =
2154+
API::moduleImport("django")
2155+
.getMember("contrib")
2156+
.getMember("postgres")
2157+
.getMember("forms")
2158+
.getMember("ranges")
2159+
.getMember([
2160+
"BaseRangeField", "DateRangeField", "DateTimeRangeField", "DecimalRangeField",
2161+
"IntegerRangeField"
2162+
])
2163+
.getASubclass*()
2164+
or
2165+
result =
2166+
API::moduleImport("django")
2167+
.getMember("forms")
2168+
.getMember("models")
2169+
.getMember(["InlineForeignKeyField", "ModelChoiceField", "ModelMultipleChoiceField"])
2170+
.getASubclass*()
2171+
}
2172+
}
2173+
}
2174+
2175+
// ---------------------------------------------------------------------------
2176+
// Helpers
2177+
// ---------------------------------------------------------------------------
19782178
/**
19792179
* Gets the last decorator call for the function `func`, if `func` has decorators.
19802180
*/
@@ -1983,6 +2183,96 @@ private module Django {
19832183
not exists(Call other_decorator | other_decorator.getArg(0) = result)
19842184
}
19852185

2186+
/** Adds the `getASelfRef` member predicate when modeling a class. */
2187+
abstract private class SelfRefMixin extends Class {
2188+
/**
2189+
* Gets a reference to instances of this class, originating from a self parameter of
2190+
* a method defined on this class.
2191+
*
2192+
* Note: TODO: This doesn't take MRO into account
2193+
* Note: TODO: This doesn't take staticmethod/classmethod into account
2194+
*/
2195+
private DataFlow::Node getASelfRef(DataFlow::TypeTracker t) {
2196+
t.start() and
2197+
result.(DataFlow::ParameterNode).getParameter() = this.getAMethod().getArg(0)
2198+
or
2199+
exists(DataFlow::TypeTracker t2 | result = this.getASelfRef(t2).track(t2, t))
2200+
}
2201+
2202+
/**
2203+
* Gets a reference to instances of this class, originating from a self parameter of
2204+
* a method defined on this class.
2205+
*
2206+
* Note: TODO: This doesn't take MRO into account
2207+
* Note: TODO: This doesn't take staticmethod/classmethod into account
2208+
*/
2209+
DataFlow::Node getASelfRef() { result = this.getASelfRef(DataFlow::TypeTracker::end()) }
2210+
}
2211+
2212+
// ---------------------------------------------------------------------------
2213+
// Form and form field modeling
2214+
// ---------------------------------------------------------------------------
2215+
/**
2216+
* A class that is a subclass of the `django.forms.Form` class,
2217+
* thereby handling user input.
2218+
*/
2219+
class DjangoFormClass extends Class, SelfRefMixin {
2220+
DjangoFormClass() { this.getABase() = Django::Forms::Form::subclassRef().getAUse().asExpr() }
2221+
}
2222+
2223+
/**
2224+
* A source of cleaned_data (either the return value from `super().clean()`, or a reference to `self.cleaned_data`)
2225+
*
2226+
* See https://docs.djangoproject.com/en/3.1/ref/forms/validation/#form-and-field-validation
2227+
*/
2228+
private class DjangoFormCleanedData extends RemoteFlowSource::Range, DataFlow::Node {
2229+
DjangoFormCleanedData() {
2230+
exists(DjangoFormClass cls, Function meth |
2231+
cls.getAMethod() = meth and
2232+
(
2233+
this = API::builtin("super").getReturn().getMember("clean").getACall() and
2234+
this.getScope() = meth
2235+
or
2236+
this.(DataFlow::AttrRead).getAttributeName() = "cleaned_data" and
2237+
this.(DataFlow::AttrRead).getObject() = cls.getASelfRef()
2238+
)
2239+
)
2240+
}
2241+
2242+
override string getSourceType() {
2243+
result = "django.forms.Field subclass, value parameter in method"
2244+
}
2245+
}
2246+
2247+
/**
2248+
* A class that is a subclass of the `django.forms.Field` class,
2249+
* thereby handling user input.
2250+
*/
2251+
class DjangoFormFieldClass extends Class {
2252+
DjangoFormFieldClass() {
2253+
this.getABase() = Django::Forms::Field::subclassRef().getAUse().asExpr()
2254+
}
2255+
}
2256+
2257+
/**
2258+
* A parameter in a method on a `DjangoFormFieldClass` that receives the user-supplied value for this field.
2259+
*
2260+
* See https://docs.djangoproject.com/en/3.1/ref/forms/validation/#form-and-field-validation
2261+
*/
2262+
private class DjangoFormFieldValueParam extends RemoteFlowSource::Range, DataFlow::ParameterNode {
2263+
DjangoFormFieldValueParam() {
2264+
exists(DjangoFormFieldClass cls, Function meth |
2265+
cls.getAMethod() = meth and
2266+
meth.getName() in ["to_python", "validate", "run_validators", "clean"] and
2267+
this.getParameter() = meth.getArg(1)
2268+
)
2269+
}
2270+
2271+
override string getSourceType() {
2272+
result = "django.forms.Field subclass, value parameter in method"
2273+
}
2274+
}
2275+
19862276
// ---------------------------------------------------------------------------
19872277
// routing modeling
19882278
// ---------------------------------------------------------------------------
@@ -2068,7 +2358,7 @@ private module Django {
20682358
}
20692359

20702360
/** A class that we consider a django View class. */
2071-
abstract class DjangoViewClass extends DjangoViewClassHelper {
2361+
abstract class DjangoViewClass extends DjangoViewClassHelper, SelfRefMixin {
20722362
/** Gets a function that could handle incoming requests, if any. */
20732363
Function getARequestHandler() {
20742364
// TODO: This doesn't handle attribute assignment. Should be OK, but analysis is not as complete as with
@@ -2080,29 +2370,6 @@ private module Django {
20802370
result.getName() = "get_redirect_url"
20812371
)
20822372
}
2083-
2084-
/**
2085-
* Gets a reference to instances of this class, originating from a self parameter of
2086-
* a method defined on this class.
2087-
*
2088-
* Note: TODO: This doesn't take MRO into account
2089-
* Note: TODO: This doesn't take staticmethod/classmethod into account
2090-
*/
2091-
private DataFlow::Node getASelfRef(DataFlow::TypeTracker t) {
2092-
t.start() and
2093-
result.(DataFlow::ParameterNode).getParameter() = this.getAMethod().getArg(0)
2094-
or
2095-
exists(DataFlow::TypeTracker t2 | result = this.getASelfRef(t2).track(t2, t))
2096-
}
2097-
2098-
/**
2099-
* Gets a reference to instances of this class, originating from a self parameter of
2100-
* a method defined on this class.
2101-
*
2102-
* Note: TODO: This doesn't take MRO into account
2103-
* Note: TODO: This doesn't take staticmethod/classmethod into account
2104-
*/
2105-
DataFlow::Node getASelfRef() { result = this.getASelfRef(DataFlow::TypeTracker::end()) }
21062373
}
21072374

21082375
/**

python/ql/test/library-tests/frameworks/django-v2-v3/TestTaint.expected

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
11
| response_test.py:61 | ok | get_redirect_url | foo |
2+
| taint_forms.py:6 | ok | to_python | value |
3+
| taint_forms.py:9 | ok | validate | value |
4+
| taint_forms.py:12 | ok | run_validators | value |
5+
| taint_forms.py:15 | ok | clean | value |
6+
| taint_forms.py:33 | ok | clean | cleaned_data |
7+
| taint_forms.py:34 | ok | clean | cleaned_data["key"] |
8+
| taint_forms.py:35 | ok | clean | cleaned_data.get(..) |
9+
| taint_forms.py:39 | ok | clean | self.cleaned_data |
10+
| taint_forms.py:40 | ok | clean | self.cleaned_data["key"] |
11+
| taint_forms.py:41 | ok | clean | self.cleaned_data.get(..) |
12+
| taint_forms.py:46 | ok | clean_foo | self.cleaned_data |
213
| taint_test.py:8 | ok | test_taint | bar |
314
| taint_test.py:8 | ok | test_taint | foo |
415
| taint_test.py:9 | ok | test_taint | baz |

0 commit comments

Comments
 (0)