Skip to content

Commit 1e9840d

Browse files
committed
python: broaden local protection concept
1 parent 179f77b commit 1e9840d

File tree

5 files changed

+34
-24
lines changed

5 files changed

+34
-24
lines changed

python/ql/lib/semmle/python/Concepts.qll

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ class CsrfProtectionSetting extends DataFlow::Node instanceof CsrfProtectionSett
123123
/** Provides a class for modeling new CSRF protection setting APIs. */
124124
module CsrfProtectionSetting {
125125
/**
126-
* A data-flow node that may set or unset Cross-site request forgery protection
126+
* A data-flow node that enables or disables Cross-site request forgery protection
127127
* in a global manner.
128128
*
129129
* Extend this class to model new APIs. If you want to refine existing API models,
@@ -139,35 +139,39 @@ module CsrfProtectionSetting {
139139
}
140140

141141
/**
142-
* A data-flow node that provides Cross-site request forgery protection
142+
* A data-flow node that enables or disables Cross-site request forgery protection
143143
* for a specific part of an application.
144144
*
145145
* Extend this class to refine existing API models. If you want to model new APIs,
146-
* extend `CsrfLocalProtection::Range` instead.
146+
* extend `CsrfLocalProtectionSetting::Range` instead.
147147
*/
148-
class CsrfLocalProtection extends DataFlow::Node instanceof CsrfLocalProtection::Range {
148+
class CsrfLocalProtectionSetting extends DataFlow::Node instanceof CsrfLocalProtectionSetting::Range {
149149
/**
150-
* Gets a `Function` representing the protected interaction
151-
* (probably a request handler).
150+
* Gets a request handler whose CSRF protection is changed.
152151
*/
153-
Function getProtected() { result = super.getProtected() }
152+
Function getRequestHandler() { result = super.getRequestHandler() }
153+
154+
/** Holds if CSRF protection is enabled by this setting */
155+
predicate csrfEnabled() { super.csrfEnabled() }
154156
}
155157

156158
/** Provides a class for modeling new CSRF protection setting APIs. */
157-
module CsrfLocalProtection {
159+
module CsrfLocalProtectionSetting {
158160
/**
159-
* A data-flow node that provides Cross-site request forgery protection
161+
* A data-flow node that enables or disables Cross-site request forgery protection
160162
* for a specific part of an application.
161163
*
162164
* Extend this class to model new APIs. If you want to refine existing API models,
163-
* extend `CsrfLocalProtection` instead.
165+
* extend `CsrfLocalProtectionSetting` instead.
164166
*/
165167
abstract class Range extends DataFlow::Node {
166168
/**
167-
* Gets a `Function` representing the protected interaction
168-
* (probably a request handler).
169+
* Gets a request handler whose CSRF protection is changed.
169170
*/
170-
abstract Function getProtected();
171+
abstract Function getRequestHandler();
172+
173+
/** Holds if CSRF protection is enabled by this setting */
174+
abstract predicate csrfEnabled();
171175
}
172176
}
173177

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2356,20 +2356,24 @@ module PrivateDjango {
23562356
}
23572357
}
23582358

2359-
private class DjangoCsrfDecorator extends CsrfLocalProtection::Range {
2359+
private class DjangoCsrfDecorator extends CsrfLocalProtectionSetting::Range {
2360+
string decoratorName;
23602361
Function function;
23612362

23622363
DjangoCsrfDecorator() {
2364+
decoratorName in ["csrf_protect", "csrf_exempt", "requires_csrf_token", "ensure_csrf_cookie"] and
23632365
this =
23642366
API::moduleImport("django")
23652367
.getMember("views")
23662368
.getMember("decorators")
23672369
.getMember("csrf")
2368-
.getMember("csrf_protect")
2370+
.getMember(decoratorName)
23692371
.getAUse() and
23702372
this.asExpr() = function.getADecorator()
23712373
}
23722374

2373-
override Function getProtected() { result = function }
2375+
override Function getRequestHandler() { result = function }
2376+
2377+
override predicate csrfEnabled() { decoratorName in ["csrf_protect", "requires_csrf_token"] }
23742378
}
23752379
}

python/ql/src/Security/CWE-352/CSRFProtectionDisabled.ql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import semmle.python.Concepts
1717
from CsrfProtectionSetting s
1818
where
1919
s.getVerificationSetting() = false and
20-
not exists(CsrfLocalProtection p) and
20+
not exists(CsrfLocalProtectionSetting p | p.csrfEnabled()) and
2121
// rule out test code as this is a common place to turn off CSRF protection
2222
not s.getLocation().getFile().getAbsolutePath().matches("%test%")
2323
select s, "Potential CSRF vulnerability due to forgery protection being disabled or weakened."

python/ql/test/experimental/meta/ConceptsTest.qll

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -520,18 +520,20 @@ class CsrfProtectionSettingTest extends InlineExpectationsTest {
520520
}
521521
}
522522

523-
class CsrfLocalProtectionTest extends InlineExpectationsTest {
524-
CsrfLocalProtectionTest() { this = "CsrfLocalProtectionTest" }
523+
class CsrfLocalProtectionSettingTest extends InlineExpectationsTest {
524+
CsrfLocalProtectionSettingTest() { this = "CsrfLocalProtectionSettingTest" }
525525

526-
override string getARelevantTag() { result = "CsrfLocalProtection" }
526+
override string getARelevantTag() { result = "CsrfLocalProtection" + ["Enabled", "Disabled"] }
527527

528528
override predicate hasActualResult(Location location, string element, string tag, string value) {
529529
exists(location.getFile().getRelativePath()) and
530-
exists(CsrfLocalProtection p |
530+
exists(CsrfLocalProtectionSetting p |
531531
location = p.getLocation() and
532532
element = p.toString() and
533-
value = p.getProtected().getName().toString() and
534-
tag = "CsrfLocalProtection"
533+
value = p.getRequestHandler().getName().toString() and
534+
if p.csrfEnabled()
535+
then tag = "CsrfLocalProtectionEnabled"
536+
else tag = "CsrfLocalProtectionDisabled"
535537
)
536538
}
537539
}

python/ql/test/library-tests/frameworks/django-v2-v3/response_test.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ class CustomJsonResponse(JsonResponse):
118118
def __init__(self, banner, content, *args, **kwargs):
119119
super().__init__(content, *args, content_type="text/html", **kwargs)
120120

121-
@csrf_protect # $CsrfLocalProtection=safe__custom_json_response
121+
@csrf_protect # $CsrfLocalProtectionEnabled=safe__custom_json_response
122122
def safe__custom_json_response(request):
123123
return CustomJsonResponse("ACME Responses", {"foo": request.GET.get("foo")}) # $HttpResponse mimetype=application/json MISSING: responseBody=Dict SPURIOUS: responseBody="ACME Responses"
124124

0 commit comments

Comments
 (0)