Merge pull request #8340 from yoff/python/simple-csrf

python: minimal CSRF implementation
This commit is contained in:
Rasmus Wriedt Larsen
2022-05-10 13:36:38 +02:00
committed by GitHub
10 changed files with 281 additions and 2 deletions

View File

@@ -969,6 +969,76 @@ module HTTP {
abstract DataFlow::Node getValueArg();
}
}
/**
* A data-flow node that enables or disables Cross-site request forgery protection
* in a global manner.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `CsrfProtectionSetting::Range` instead.
*/
class CsrfProtectionSetting extends DataFlow::Node instanceof CsrfProtectionSetting::Range {
/**
* Gets the boolean value corresponding to if CSRF protection is enabled
* (`true`) or disabled (`false`) by this node.
*/
boolean getVerificationSetting() { result = super.getVerificationSetting() }
}
/** Provides a class for modeling new CSRF protection setting APIs. */
module CsrfProtectionSetting {
/**
* A data-flow node that enables or disables Cross-site request forgery protection
* in a global manner.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `CsrfProtectionSetting` instead.
*/
abstract class Range extends DataFlow::Node {
/**
* Gets the boolean value corresponding to if CSRF protection is enabled
* (`true`) or disabled (`false`) by this node.
*/
abstract boolean getVerificationSetting();
}
}
/**
* A data-flow node that enables or disables Cross-site request forgery protection
* for a specific part of an application.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `CsrfLocalProtectionSetting::Range` instead.
*/
class CsrfLocalProtectionSetting extends DataFlow::Node instanceof CsrfLocalProtectionSetting::Range {
/**
* Gets a request handler whose CSRF protection is changed.
*/
Function getRequestHandler() { result = super.getRequestHandler() }
/** Holds if CSRF protection is enabled by this setting */
predicate csrfEnabled() { super.csrfEnabled() }
}
/** Provides a class for modeling new CSRF protection setting APIs. */
module CsrfLocalProtectionSetting {
/**
* A data-flow node that enables or disables Cross-site request forgery protection
* for a specific part of an application.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `CsrfLocalProtectionSetting` instead.
*/
abstract class Range extends DataFlow::Node {
/**
* Gets a request handler whose CSRF protection is changed.
*/
abstract Function getRequestHandler();
/** Holds if CSRF protection is enabled by this setting */
abstract predicate csrfEnabled();
}
}
}
/** Provides classes for modeling HTTP clients. */

View File

@@ -2738,4 +2738,67 @@ module PrivateDjango {
.getAnImmediateUse()
}
}
// ---------------------------------------------------------------------------
// Settings
// ---------------------------------------------------------------------------
/**
* A custom middleware stack
*/
private class DjangoSettingsMiddlewareStack extends HTTP::Server::CsrfProtectionSetting::Range {
List list;
DjangoSettingsMiddlewareStack() {
this.asExpr() = list and
// we look for an assignment to the `MIDDLEWARE` setting
exists(DataFlow::Node mw |
mw.asVar().getName() = "MIDDLEWARE" and
DataFlow::localFlow(this, mw)
|
// To only include results where CSRF protection matters, we only care about CSRF
// protection when the django authentication middleware is enabled.
// Since an active session cookie is exactly what would allow an attacker to perform
// a CSRF attack.
// Notice that this does not ensure that this is not a FP, since the authentication
// middleware might be unused.
//
// This also strongly implies that `mw` is in fact a Django middleware setting and
// not just a variable named `MIDDLEWARE`.
list.getAnElt().(StrConst).getText() =
"django.contrib.auth.middleware.AuthenticationMiddleware"
)
}
override boolean getVerificationSetting() {
if
list.getAnElt().(StrConst).getText() in [
"django.middleware.csrf.CsrfViewMiddleware",
// see https://github.com/mozilla/django-session-csrf
"session_csrf.CsrfMiddleware"
]
then result = true
else result = false
}
}
private class DjangoCsrfDecorator extends HTTP::Server::CsrfLocalProtectionSetting::Range {
string decoratorName;
Function function;
DjangoCsrfDecorator() {
decoratorName in ["csrf_protect", "csrf_exempt", "requires_csrf_token", "ensure_csrf_cookie"] and
this =
API::moduleImport("django")
.getMember("views")
.getMember("decorators")
.getMember("csrf")
.getMember(decoratorName)
.getAUse() and
this.asExpr() = function.getADecorator()
}
override Function getRequestHandler() { result = function }
override predicate csrfEnabled() { decoratorName in ["csrf_protect", "requires_csrf_token"] }
}
}

View File

@@ -0,0 +1,60 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Cross-site request forgery (CSRF) is a type of vulnerability in which an
attacker is able to force a user to carry out an action that the user did
not intend.
</p>
<p>
The attacker tricks an authenticated user into submitting a request to the
web application. Typically this request will result in a state change on
the server, such as changing the user's password. The request can be
initiated when the user visits a site controlled by the attacker. If the
web application relies only on cookies for authentication, or on other
credentials that are automatically included in the request, then this
request will appear as legitimate to the server.
</p>
<p>
A common countermeasure for CSRF is to generate a unique token to be
included in the HTML sent from the server to a user. This token can be
used as a hidden field to be sent back with requests to the server, where
the server can then check that the token is valid and associated with the
relevant user session.
</p>
</overview>
<recommendation>
<p>
In many web frameworks, CSRF protection is enabled by default. In these
cases, using the default configuration is sufficient to guard against most
CSRF attacks.
</p>
</recommendation>
<example>
<p>
The following example shows a case where CSRF protection is disabled by
overriding the default middleware stack and not including the one protecting against CSRF.
</p>
<sample src="examples/settings.py"/>
<p>
The protecting middleware was probably commented out during a testing phase, when server-side token generation was not set up.
Simply commenting it back in will enable CSRF protection.
</p>
</example>
<references>
<li>Wikipedia: <a href="https://en.wikipedia.org/wiki/Cross-site_request_forgery">Cross-site request forgery</a></li>
<li>OWASP: <a href="https://owasp.org/www-community/attacks/csrf">Cross-site request forgery</a></li>
</references>
</qhelp>

View File

@@ -0,0 +1,37 @@
/**
* @name CSRF protection weakened or disabled
* @description Disabling or weakening CSRF protection may make the application
* vulnerable to a Cross-Site Request Forgery (CSRF) attack.
* @kind problem
* @problem.severity warning
* @security-severity 8.8
* @precision high
* @id py/csrf-protection-disabled
* @tags security
* external/cwe/cwe-352
*/
import python
import semmle.python.Concepts
predicate relevantSetting(HTTP::Server::CsrfProtectionSetting s) {
// rule out test code as this is a common place to turn off CSRF protection.
// We don't use normal `TestScope` to find test files, since we also want to match
// a settings file such as `.../integration-tests/settings.py`
not s.getLocation().getFile().getAbsolutePath().matches("%test%")
}
predicate vulnerableSetting(HTTP::Server::CsrfProtectionSetting s) {
s.getVerificationSetting() = false and
not exists(HTTP::Server::CsrfLocalProtectionSetting p | p.csrfEnabled()) and
relevantSetting(s)
}
from HTTP::Server::CsrfProtectionSetting setting
where
vulnerableSetting(setting) and
// We have seen examples of dummy projects with vulnerable settings alongside a main
// project with a protecting settings file. We want to rule out this scenario, so we
// require all non-test settings to be vulnerable.
forall(HTTP::Server::CsrfProtectionSetting s | relevantSetting(s) | vulnerableSetting(s))
select setting, "Potential CSRF vulnerability due to forgery protection being disabled or weakened."

View File

@@ -0,0 +1,9 @@
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
# 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

View File

@@ -0,0 +1,4 @@
---
category: newQuery
---
* The query "CSRF protection weakened or disabled" (`py/csrf-protection-disabled`) has been implemented. Its results will now appear by default.

View File

@@ -540,6 +540,40 @@ class HttpClientRequestTest extends InlineExpectationsTest {
}
}
class CsrfProtectionSettingTest extends InlineExpectationsTest {
CsrfProtectionSettingTest() { this = "CsrfProtectionSettingTest" }
override string getARelevantTag() { result = "CsrfProtectionSetting" }
override predicate hasActualResult(Location location, string element, string tag, string value) {
exists(location.getFile().getRelativePath()) and
exists(HTTP::Server::CsrfProtectionSetting setting |
location = setting.getLocation() and
element = setting.toString() and
value = setting.getVerificationSetting().toString() and
tag = "CsrfProtectionSetting"
)
}
}
class CsrfLocalProtectionSettingTest extends InlineExpectationsTest {
CsrfLocalProtectionSettingTest() { this = "CsrfLocalProtectionSettingTest" }
override string getARelevantTag() { result = "CsrfLocalProtection" + ["Enabled", "Disabled"] }
override predicate hasActualResult(Location location, string element, string tag, string value) {
exists(location.getFile().getRelativePath()) and
exists(HTTP::Server::CsrfLocalProtectionSetting p |
location = p.getLocation() and
element = p.toString() and
value = p.getRequestHandler().getName().toString() and
if p.csrfEnabled()
then tag = "CsrfLocalProtectionEnabled"
else tag = "CsrfLocalProtectionDisabled"
)
}
}
class XmlParsingTest extends InlineExpectationsTest {
XmlParsingTest() { this = "XmlParsingTest" }

View File

@@ -1,5 +1,6 @@
from django.http.response import HttpResponse, HttpResponseRedirect, HttpResponsePermanentRedirect, JsonResponse, HttpResponseNotFound
from django.views.generic import RedirectView
from django.views.decorators.csrf import csrf_protect
import django.shortcuts
import json
@@ -117,6 +118,7 @@ class CustomJsonResponse(JsonResponse):
def __init__(self, banner, content, *args, **kwargs):
super().__init__(content, *args, content_type="text/html", **kwargs)
@csrf_protect # $CsrfLocalProtectionEnabled=safe__custom_json_response
def safe__custom_json_response(request):
return CustomJsonResponse("ACME Responses", {"foo": request.GET.get("foo")}) # $HttpResponse mimetype=application/json MISSING: responseBody=Dict SPURIOUS: responseBody="ACME Responses"

View File

@@ -40,7 +40,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
]
MIDDLEWARE = [
MIDDLEWARE = [ # $CsrfProtectionSetting=false
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',