Cross-domain CSRF cookies
When configured, set an additional cookie with the CSRF token for use by subdomains. The cookie can have a different name than the default CSRF cookie, preventing conflicts between cookies from different domains (e.g. ".edx.org", "courses.edx.org", and "edge.edx.org"). The new cookie is included only on the enrollment API views so that the scope of this change is limited to the end-points that require cross-domain POST requests.
This commit is contained in:
28
common/djangoapps/cors_csrf/decorators.py
Normal file
28
common/djangoapps/cors_csrf/decorators.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""Decorators for cross-domain CSRF. """
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
|
||||
|
||||
def ensure_csrf_cookie_cross_domain(func):
|
||||
"""View decorator for sending a cross-domain CSRF cookie.
|
||||
|
||||
This works like Django's `@ensure_csrf_cookie`, but
|
||||
will also set an additional CSRF cookie for use
|
||||
cross-domain.
|
||||
|
||||
Arguments:
|
||||
func (function): The view function to decorate.
|
||||
|
||||
"""
|
||||
def _inner(*args, **kwargs): # pylint: disable=missing-docstring
|
||||
if args:
|
||||
# Set the META `CROSS_DOMAIN_CSRF_COOKIE_USED` flag so
|
||||
# that `CsrfCrossDomainCookieMiddleware` knows to set
|
||||
# the cross-domain version of the CSRF cookie.
|
||||
request = args[0]
|
||||
request.META['CROSS_DOMAIN_CSRF_COOKIE_USED'] = True
|
||||
|
||||
# Decorate the request with Django's
|
||||
# `ensure_csrf_cookie` to ensure that the usual
|
||||
# CSRF cookie gets set.
|
||||
return ensure_csrf_cookie(func)(*args, **kwargs)
|
||||
return _inner
|
||||
@@ -1,6 +1,10 @@
|
||||
"""
|
||||
Middleware for handling CSRF checks with CORS requests
|
||||
|
||||
|
||||
CSRF and referrer domain checks
|
||||
-------------------------------
|
||||
|
||||
When processing HTTPS requests, the default CSRF middleware checks that the referer
|
||||
domain and protocol is the same as the request's domain and protocol. This is meant
|
||||
to avoid a type of attack for sites which serve their content with both HTTP and HTTPS,
|
||||
@@ -15,6 +19,27 @@ middle attack vector.
|
||||
We thus do the CSRF check of requests coming from an authorized CORS host separately
|
||||
in this middleware, applying the same protections as the default CSRF middleware, but
|
||||
without the referrer check, when both the request and the referer use HTTPS.
|
||||
|
||||
|
||||
CSRF cookie domains
|
||||
-------------------
|
||||
|
||||
In addition, in order to make cross-domain AJAX calls to CSRF-protected end-points,
|
||||
we need to send the CSRF token in the HTTP header of the request.
|
||||
|
||||
The simple way to do this would be to set the CSRF_COOKIE_DOMAIN to ".edx.org",
|
||||
but unfortunately this can cause problems. For example, suppose that
|
||||
"first.edx.org" sets the cookie with domain ".edx.org", but "second.edx.org"
|
||||
sets a cookie with domain "second.edx.org". In this case, the browser
|
||||
would have two different CSRF tokens set (one for each cookie domain),
|
||||
which can cause non-deterministic failures depending on which cookie
|
||||
is sent first.
|
||||
|
||||
For this reason, we add a second cookie that (a) has the domain set to ".edx.org",
|
||||
but (b) does NOT have the same name as the CSRF_COOKIE_NAME. Clients making
|
||||
cross-domain requests can use this cookie instead of the subdomain-specific
|
||||
CSRF cookie.
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -22,35 +47,91 @@ import urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.middleware.csrf import CsrfViewMiddleware
|
||||
from django.core.exceptions import MiddlewareNotUsed, ImproperlyConfigured
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_cross_domain_request_allowed(request):
|
||||
"""Check whether we should allow the cross-domain request.
|
||||
|
||||
We allow a cross-domain request only if:
|
||||
|
||||
1) The request is made securely and the referer has "https://" as the protocol.
|
||||
2) The referer domain has been whitelisted.
|
||||
|
||||
Arguments:
|
||||
request (HttpRequest)
|
||||
|
||||
Returns:
|
||||
bool
|
||||
|
||||
"""
|
||||
referer = request.META.get('HTTP_REFERER')
|
||||
referer_parts = urlparse.urlparse(referer) if referer else None
|
||||
referer_hostname = referer_parts.hostname if referer_parts is not None else None
|
||||
|
||||
# Use CORS_ALLOW_INSECURE *only* for development and testing environments;
|
||||
# it should never be enabled in production.
|
||||
if not getattr(settings, 'CORS_ALLOW_INSECURE', False):
|
||||
if not request.is_secure():
|
||||
log.debug(
|
||||
u"Request is not secure, so we cannot send the CSRF token. "
|
||||
u"For testing purposes, you can disable this check by setting "
|
||||
u"`CORS_ALLOW_INSECURE` to True in the settings"
|
||||
)
|
||||
return False
|
||||
|
||||
if not referer:
|
||||
log.debug(u"No referer provided over a secure connection, so we cannot check the protocol.")
|
||||
return False
|
||||
|
||||
if not referer_parts.scheme == 'https':
|
||||
log.debug(u"Referer '%s' must have the scheme 'https'")
|
||||
return False
|
||||
|
||||
domain_is_whitelisted = (
|
||||
getattr(settings, 'CORS_ORIGIN_ALLOW_ALL', False) or
|
||||
referer_hostname in getattr(settings, 'CORS_ORIGIN_WHITELIST', [])
|
||||
)
|
||||
if not domain_is_whitelisted:
|
||||
if referer_hostname is None:
|
||||
# If no referer is specified, we can't check if it's a cross-domain
|
||||
# request or not.
|
||||
log.debug(u"Referrer hostname is `None`, so it is not on the whitelist.")
|
||||
elif referer_hostname != request.get_host():
|
||||
log.warning(
|
||||
(
|
||||
u"Domain '%s' is not on the cross domain whitelist. "
|
||||
u"Add the domain to `CORS_ORIGIN_WHITELIST` or set "
|
||||
u"`CORS_ORIGIN_ALLOW_ALL` to True in the settings."
|
||||
), referer_hostname
|
||||
)
|
||||
else:
|
||||
log.debug(
|
||||
(
|
||||
u"Domain '%s' is the same as the hostname in the request, "
|
||||
u"so we are not going to treat it as a cross-domain request."
|
||||
), referer_hostname
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class CorsCSRFMiddleware(CsrfViewMiddleware):
|
||||
"""
|
||||
Middleware for handling CSRF checks with CORS requests
|
||||
"""
|
||||
def is_enabled(self, request):
|
||||
"""
|
||||
Override the `is_enabled()` method to allow cross-domain HTTPS requests
|
||||
"""
|
||||
def __init__(self):
|
||||
"""Disable the middleware if the feature flag is disabled. """
|
||||
if not settings.FEATURES.get('ENABLE_CORS_HEADERS'):
|
||||
return False
|
||||
|
||||
referer = request.META.get('HTTP_REFERER')
|
||||
if not referer:
|
||||
return False
|
||||
referer_parts = urlparse.urlparse(referer)
|
||||
|
||||
if referer_parts.hostname not in getattr(settings, 'CORS_ORIGIN_WHITELIST', []):
|
||||
return False
|
||||
if not request.is_secure() or referer_parts.scheme != 'https':
|
||||
return False
|
||||
|
||||
return True
|
||||
raise MiddlewareNotUsed()
|
||||
|
||||
def process_view(self, request, callback, callback_args, callback_kwargs):
|
||||
if not self.is_enabled(request):
|
||||
"""Skip the usual CSRF referer check if this is an allowed cross-domain request. """
|
||||
if not is_cross_domain_request_allowed(request):
|
||||
log.debug("Could not disable CSRF middleware referer check for cross-domain request.")
|
||||
return
|
||||
|
||||
is_secure_default = request.is_secure
|
||||
@@ -65,3 +146,77 @@ class CorsCSRFMiddleware(CsrfViewMiddleware):
|
||||
res = super(CorsCSRFMiddleware, self).process_view(request, callback, callback_args, callback_kwargs)
|
||||
request.is_secure = is_secure_default
|
||||
return res
|
||||
|
||||
|
||||
class CsrfCrossDomainCookieMiddleware(object):
|
||||
"""Set an additional "cross-domain" CSRF cookie.
|
||||
|
||||
Usage:
|
||||
|
||||
1) Decorate a view with `@ensure_csrf_cookie_cross_domain`.
|
||||
2) Set `CROSS_DOMAIN_CSRF_COOKIE_NAME` and `CROSS_DOMAIN_CSRF_COOKIE_DOMAIN`
|
||||
in settings.
|
||||
3) Add the domain to `CORS_ORIGIN_WHITELIST`
|
||||
4) Enable `FEATURES['ENABLE_CROSS_DOMAIN_CSRF_COOKIE']`
|
||||
|
||||
For testing, it is often easier to relax the security checks by setting:
|
||||
* `CORS_ALLOW_INSECURE = True`
|
||||
* `CORS_ORIGIN_ALLOW_ALL = True`
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""Disable the middleware if the feature is not enabled. """
|
||||
if not settings.FEATURES.get('ENABLE_CROSS_DOMAIN_CSRF_COOKIE'):
|
||||
raise MiddlewareNotUsed()
|
||||
|
||||
if not getattr(settings, 'CROSS_DOMAIN_CSRF_COOKIE_NAME', ''):
|
||||
raise ImproperlyConfigured(
|
||||
"You must set `CROSS_DOMAIN_CSRF_COOKIE_NAME` when "
|
||||
"`FEATURES['ENABLE_CROSS_DOMAIN_CSRF_COOKIE']` is True."
|
||||
)
|
||||
|
||||
if not getattr(settings, 'CROSS_DOMAIN_CSRF_COOKIE_DOMAIN', ''):
|
||||
raise ImproperlyConfigured(
|
||||
"You must set `CROSS_DOMAIN_CSRF_COOKIE_DOMAIN` when "
|
||||
"`FEATURES['ENABLE_CROSS_DOMAIN_CSRF_COOKIE']` is True."
|
||||
)
|
||||
|
||||
def process_response(self, request, response):
|
||||
"""Set the cross-domain CSRF cookie. """
|
||||
|
||||
# Check whether this is a secure request from a domain on our whitelist.
|
||||
if not is_cross_domain_request_allowed(request):
|
||||
log.debug("Could not set cross-domain CSRF cookie.")
|
||||
return response
|
||||
|
||||
# Check whether (a) the CSRF middleware has already set a cookie, and
|
||||
# (b) this is a view decorated with `@ensure_cross_domain_csrf_cookie`
|
||||
# If so, we can send the cross-domain CSRF cookie.
|
||||
should_set_cookie = (
|
||||
request.META.get('CROSS_DOMAIN_CSRF_COOKIE_USED', False) and
|
||||
request.META.get('CSRF_COOKIE_USED', False) and
|
||||
request.META.get('CSRF_COOKIE') is not None
|
||||
)
|
||||
|
||||
if should_set_cookie:
|
||||
# This is very similar to the code in Django's CSRF middleware
|
||||
# implementation, with two exceptions:
|
||||
# 1) We change the cookie name and domain so it can be used cross-domain.
|
||||
# 2) We always set "secure" to True, so that the CSRF token must be
|
||||
# sent over a secure connection.
|
||||
response.set_cookie(
|
||||
settings.CROSS_DOMAIN_CSRF_COOKIE_NAME,
|
||||
request.META['CSRF_COOKIE'],
|
||||
max_age=settings.CSRF_COOKIE_AGE,
|
||||
domain=settings.CROSS_DOMAIN_CSRF_COOKIE_DOMAIN,
|
||||
path=settings.CSRF_COOKIE_PATH,
|
||||
secure=True
|
||||
)
|
||||
log.debug(
|
||||
"Set cross-domain CSRF cookie '%s' for domain '%s'",
|
||||
settings.CROSS_DOMAIN_CSRF_COOKIE_NAME,
|
||||
settings.CROSS_DOMAIN_CSRF_COOKIE_DOMAIN
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
@@ -1,101 +0,0 @@
|
||||
"""
|
||||
Tests for the CORS CSRF middleware
|
||||
"""
|
||||
|
||||
from mock import patch, Mock
|
||||
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.middleware.csrf import CsrfViewMiddleware
|
||||
|
||||
from cors_csrf.middleware import CorsCSRFMiddleware
|
||||
|
||||
|
||||
SENTINEL = object()
|
||||
|
||||
|
||||
class TestCorsMiddlewareProcessRequest(TestCase):
|
||||
"""
|
||||
Test processing a request through the middleware
|
||||
"""
|
||||
def get_request(self, is_secure, http_referer):
|
||||
"""
|
||||
Build a test request
|
||||
"""
|
||||
request = Mock()
|
||||
request.META = {'HTTP_REFERER': http_referer}
|
||||
request.is_secure = lambda: is_secure
|
||||
return request
|
||||
|
||||
def setUp(self):
|
||||
self.middleware = CorsCSRFMiddleware()
|
||||
|
||||
def check_not_enabled(self, request):
|
||||
"""
|
||||
Check that the middleware does NOT process the provided request
|
||||
"""
|
||||
with patch.object(CsrfViewMiddleware, 'process_view') as mock_method:
|
||||
res = self.middleware.process_view(request, None, None, None)
|
||||
|
||||
self.assertIsNone(res)
|
||||
self.assertFalse(mock_method.called)
|
||||
|
||||
def check_enabled(self, request):
|
||||
"""
|
||||
Check that the middleware does process the provided request
|
||||
"""
|
||||
def cb_check_req_is_secure_false(request, callback, args, kwargs):
|
||||
"""
|
||||
Check that the request doesn't pass (yet) the `is_secure()` test
|
||||
"""
|
||||
self.assertFalse(request.is_secure())
|
||||
return SENTINEL
|
||||
|
||||
with patch.object(CsrfViewMiddleware, 'process_view') as mock_method:
|
||||
mock_method.side_effect = cb_check_req_is_secure_false
|
||||
res = self.middleware.process_view(request, None, None, None)
|
||||
|
||||
self.assertIs(res, SENTINEL)
|
||||
self.assertTrue(request.is_secure())
|
||||
|
||||
@override_settings(FEATURES={'ENABLE_CORS_HEADERS': True},
|
||||
CORS_ORIGIN_WHITELIST=['foo.com'])
|
||||
def test_enabled(self):
|
||||
request = self.get_request(is_secure=True,
|
||||
http_referer='https://foo.com/bar')
|
||||
self.check_enabled(request)
|
||||
|
||||
@override_settings(FEATURES={'ENABLE_CORS_HEADERS': False},
|
||||
CORS_ORIGIN_WHITELIST=['foo.com'])
|
||||
def test_disabled_no_cors_headers(self):
|
||||
request = self.get_request(is_secure=True,
|
||||
http_referer='https://foo.com/bar')
|
||||
self.check_not_enabled(request)
|
||||
|
||||
@override_settings(FEATURES={'ENABLE_CORS_HEADERS': True},
|
||||
CORS_ORIGIN_WHITELIST=['bar.com'])
|
||||
def test_disabled_wrong_cors_domain(self):
|
||||
request = self.get_request(is_secure=True,
|
||||
http_referer='https://foo.com/bar')
|
||||
self.check_not_enabled(request)
|
||||
|
||||
@override_settings(FEATURES={'ENABLE_CORS_HEADERS': True},
|
||||
CORS_ORIGIN_WHITELIST=['foo.com'])
|
||||
def test_disabled_wrong_cors_domain_reversed(self):
|
||||
request = self.get_request(is_secure=True,
|
||||
http_referer='https://bar.com/bar')
|
||||
self.check_not_enabled(request)
|
||||
|
||||
@override_settings(FEATURES={'ENABLE_CORS_HEADERS': True},
|
||||
CORS_ORIGIN_WHITELIST=['foo.com'])
|
||||
def test_disabled_http_request(self):
|
||||
request = self.get_request(is_secure=False,
|
||||
http_referer='https://foo.com/bar')
|
||||
self.check_not_enabled(request)
|
||||
|
||||
@override_settings(FEATURES={'ENABLE_CORS_HEADERS': True},
|
||||
CORS_ORIGIN_WHITELIST=['foo.com'])
|
||||
def test_disabled_http_referer(self):
|
||||
request = self.get_request(is_secure=True,
|
||||
http_referer='http://foo.com/bar')
|
||||
self.check_not_enabled(request)
|
||||
0
common/djangoapps/cors_csrf/tests/__init__.py
Normal file
0
common/djangoapps/cors_csrf/tests/__init__.py
Normal file
24
common/djangoapps/cors_csrf/tests/test_decorators.py
Normal file
24
common/djangoapps/cors_csrf/tests/test_decorators.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""Tests for cross-domain CSRF decorators. """
|
||||
import json
|
||||
import mock
|
||||
from django.http import HttpResponse
|
||||
from django.test import TestCase
|
||||
from cors_csrf.decorators import ensure_csrf_cookie_cross_domain
|
||||
|
||||
|
||||
def fake_view(request):
|
||||
"""Fake view that returns the request META as a JSON-encoded string. """
|
||||
return HttpResponse(json.dumps(request.META))
|
||||
|
||||
|
||||
class TestEnsureCsrfCookieCrossDomain(TestCase):
|
||||
"""Test the `ensucre_csrf_cookie_cross_domain` decorator. """
|
||||
|
||||
def test_ensure_csrf_cookie_cross_domain(self):
|
||||
request = mock.Mock()
|
||||
request.META = {}
|
||||
wrapped_view = ensure_csrf_cookie_cross_domain(fake_view)
|
||||
response = wrapped_view(request)
|
||||
response_meta = json.loads(response.content)
|
||||
self.assertEqual(response_meta['CROSS_DOMAIN_CSRF_COOKIE_USED'], True)
|
||||
self.assertEqual(response_meta['CSRF_COOKIE_USED'], True)
|
||||
275
common/djangoapps/cors_csrf/tests/test_middleware.py
Normal file
275
common/djangoapps/cors_csrf/tests/test_middleware.py
Normal file
@@ -0,0 +1,275 @@
|
||||
"""
|
||||
Tests for the CORS CSRF middleware
|
||||
"""
|
||||
|
||||
from mock import patch, Mock
|
||||
import ddt
|
||||
|
||||
from django.test import TestCase
|
||||
from django.test.utils import override_settings
|
||||
from django.core.exceptions import MiddlewareNotUsed, ImproperlyConfigured
|
||||
from django.http import HttpResponse
|
||||
from django.middleware.csrf import CsrfViewMiddleware
|
||||
|
||||
from cors_csrf.middleware import CorsCSRFMiddleware, CsrfCrossDomainCookieMiddleware
|
||||
|
||||
|
||||
SENTINEL = object()
|
||||
|
||||
|
||||
class TestCorsMiddlewareProcessRequest(TestCase):
|
||||
"""
|
||||
Test processing a request through the middleware
|
||||
"""
|
||||
def get_request(self, is_secure, http_referer):
|
||||
"""
|
||||
Build a test request
|
||||
"""
|
||||
request = Mock()
|
||||
request.META = {'HTTP_REFERER': http_referer}
|
||||
request.is_secure = lambda: is_secure
|
||||
return request
|
||||
|
||||
@override_settings(FEATURES={'ENABLE_CORS_HEADERS': True})
|
||||
def setUp(self):
|
||||
super(TestCorsMiddlewareProcessRequest, self).setUp()
|
||||
self.middleware = CorsCSRFMiddleware()
|
||||
|
||||
def check_not_enabled(self, request):
|
||||
"""
|
||||
Check that the middleware does NOT process the provided request
|
||||
"""
|
||||
with patch.object(CsrfViewMiddleware, 'process_view') as mock_method:
|
||||
res = self.middleware.process_view(request, None, None, None)
|
||||
|
||||
self.assertIsNone(res)
|
||||
self.assertFalse(mock_method.called)
|
||||
|
||||
def check_enabled(self, request):
|
||||
"""
|
||||
Check that the middleware does process the provided request
|
||||
"""
|
||||
def cb_check_req_is_secure_false(request, callback, args, kwargs):
|
||||
"""
|
||||
Check that the request doesn't pass (yet) the `is_secure()` test
|
||||
"""
|
||||
self.assertFalse(request.is_secure())
|
||||
return SENTINEL
|
||||
|
||||
with patch.object(CsrfViewMiddleware, 'process_view') as mock_method:
|
||||
mock_method.side_effect = cb_check_req_is_secure_false
|
||||
res = self.middleware.process_view(request, None, None, None)
|
||||
|
||||
self.assertIs(res, SENTINEL)
|
||||
self.assertTrue(request.is_secure())
|
||||
|
||||
@override_settings(CORS_ORIGIN_WHITELIST=['foo.com'])
|
||||
def test_enabled(self):
|
||||
request = self.get_request(is_secure=True, http_referer='https://foo.com/bar')
|
||||
self.check_enabled(request)
|
||||
|
||||
@override_settings(
|
||||
FEATURES={'ENABLE_CORS_HEADERS': False},
|
||||
CORS_ORIGIN_WHITELIST=['foo.com']
|
||||
)
|
||||
def test_disabled_no_cors_headers(self):
|
||||
with self.assertRaises(MiddlewareNotUsed):
|
||||
CorsCSRFMiddleware()
|
||||
|
||||
@override_settings(CORS_ORIGIN_WHITELIST=['bar.com'])
|
||||
def test_disabled_wrong_cors_domain(self):
|
||||
request = self.get_request(is_secure=True, http_referer='https://foo.com/bar')
|
||||
self.check_not_enabled(request)
|
||||
|
||||
@override_settings(CORS_ORIGIN_WHITELIST=['foo.com'])
|
||||
def test_disabled_wrong_cors_domain_reversed(self):
|
||||
request = self.get_request(is_secure=True, http_referer='https://bar.com/bar')
|
||||
self.check_not_enabled(request)
|
||||
|
||||
@override_settings(CORS_ORIGIN_WHITELIST=['foo.com'])
|
||||
def test_disabled_http_request(self):
|
||||
request = self.get_request(is_secure=False, http_referer='https://foo.com/bar')
|
||||
self.check_not_enabled(request)
|
||||
|
||||
@override_settings(CORS_ORIGIN_WHITELIST=['foo.com'])
|
||||
def test_disabled_http_referer(self):
|
||||
request = self.get_request(is_secure=True, http_referer='http://foo.com/bar')
|
||||
self.check_not_enabled(request)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class TestCsrfCrossDomainCookieMiddleware(TestCase):
|
||||
"""Tests for `CsrfCrossDomainCookieMiddleware`. """
|
||||
|
||||
REFERER = 'https://www.example.com'
|
||||
COOKIE_NAME = 'shared-csrftoken'
|
||||
COOKIE_VALUE = 'abcd123'
|
||||
COOKIE_DOMAIN = '.edx.org'
|
||||
|
||||
@override_settings(
|
||||
FEATURES={'ENABLE_CROSS_DOMAIN_CSRF_COOKIE': True},
|
||||
CROSS_DOMAIN_CSRF_COOKIE_NAME=COOKIE_NAME,
|
||||
CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=COOKIE_DOMAIN
|
||||
)
|
||||
def setUp(self):
|
||||
super(TestCsrfCrossDomainCookieMiddleware, self).setUp()
|
||||
self.middleware = CsrfCrossDomainCookieMiddleware()
|
||||
|
||||
@override_settings(FEATURES={'ENABLE_CROSS_DOMAIN_CSRF_COOKIE': False})
|
||||
def test_disabled_by_feature_flag(self):
|
||||
with self.assertRaises(MiddlewareNotUsed):
|
||||
CsrfCrossDomainCookieMiddleware()
|
||||
|
||||
@ddt.data('CROSS_DOMAIN_CSRF_COOKIE_NAME', 'CROSS_DOMAIN_CSRF_COOKIE_DOMAIN')
|
||||
def test_improperly_configured(self, missing_setting):
|
||||
settings = {
|
||||
'FEATURES': {'ENABLE_CROSS_DOMAIN_CSRF_COOKIE': True},
|
||||
'CROSS_DOMAIN_CSRF_COOKIE_NAME': self.COOKIE_NAME,
|
||||
'CROSS_DOMAIN_CSRF_COOKIE_DOMAIN': self.COOKIE_DOMAIN
|
||||
}
|
||||
del settings[missing_setting]
|
||||
|
||||
with override_settings(**settings):
|
||||
with self.assertRaises(ImproperlyConfigured):
|
||||
CsrfCrossDomainCookieMiddleware()
|
||||
|
||||
@override_settings(
|
||||
CROSS_DOMAIN_CSRF_COOKIE_NAME=COOKIE_NAME,
|
||||
CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=COOKIE_DOMAIN,
|
||||
CORS_ORIGIN_ALLOW_ALL=True
|
||||
)
|
||||
def test_skip_if_not_secure(self):
|
||||
response = self._get_response(is_secure=False)
|
||||
self._assert_cookie_sent(response, False)
|
||||
|
||||
@override_settings(
|
||||
CROSS_DOMAIN_CSRF_COOKIE_NAME=COOKIE_NAME,
|
||||
CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=COOKIE_DOMAIN,
|
||||
CORS_ORIGIN_ALLOW_ALL=True
|
||||
)
|
||||
def test_skip_if_not_sending_csrf_token(self):
|
||||
response = self._get_response(csrf_cookie_used=False)
|
||||
self._assert_cookie_sent(response, False)
|
||||
|
||||
@override_settings(
|
||||
CROSS_DOMAIN_CSRF_COOKIE_NAME=COOKIE_NAME,
|
||||
CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=COOKIE_DOMAIN,
|
||||
CORS_ORIGIN_ALLOW_ALL=True
|
||||
)
|
||||
def test_skip_if_not_cross_domain_decorator(self):
|
||||
response = self._get_response(cross_domain_decorator=False)
|
||||
self._assert_cookie_sent(response, False)
|
||||
|
||||
@override_settings(
|
||||
CROSS_DOMAIN_CSRF_COOKIE_NAME=COOKIE_NAME,
|
||||
CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=COOKIE_DOMAIN,
|
||||
CORS_ORIGIN_WHITELIST=['other.example.com']
|
||||
)
|
||||
def test_skip_if_referer_not_whitelisted(self):
|
||||
response = self._get_response()
|
||||
self._assert_cookie_sent(response, False)
|
||||
|
||||
@override_settings(
|
||||
CROSS_DOMAIN_CSRF_COOKIE_NAME=COOKIE_NAME,
|
||||
CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=COOKIE_DOMAIN
|
||||
)
|
||||
def test_skip_if_not_cross_domain(self):
|
||||
response = self._get_response(
|
||||
referer="https://courses.edx.org/foo",
|
||||
host="courses.edx.org"
|
||||
)
|
||||
self._assert_cookie_sent(response, False)
|
||||
|
||||
@override_settings(
|
||||
CROSS_DOMAIN_CSRF_COOKIE_NAME=COOKIE_NAME,
|
||||
CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=COOKIE_DOMAIN,
|
||||
CORS_ORIGIN_ALLOW_ALL=True
|
||||
)
|
||||
def test_skip_if_no_referer(self):
|
||||
response = self._get_response(delete_referer=True)
|
||||
self._assert_cookie_sent(response, False)
|
||||
|
||||
@override_settings(
|
||||
CROSS_DOMAIN_CSRF_COOKIE_NAME=COOKIE_NAME,
|
||||
CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=COOKIE_DOMAIN,
|
||||
CORS_ORIGIN_ALLOW_ALL=True
|
||||
)
|
||||
def test_skip_if_referer_not_https(self):
|
||||
response = self._get_response(referer="http://www.example.com")
|
||||
self._assert_cookie_sent(response, False)
|
||||
|
||||
@override_settings(
|
||||
CROSS_DOMAIN_CSRF_COOKIE_NAME=COOKIE_NAME,
|
||||
CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=COOKIE_DOMAIN,
|
||||
CORS_ORIGIN_ALLOW_ALL=True
|
||||
)
|
||||
def test_skip_if_referer_no_protocol(self):
|
||||
response = self._get_response(referer="example.com")
|
||||
self._assert_cookie_sent(response, False)
|
||||
|
||||
@override_settings(
|
||||
CROSS_DOMAIN_CSRF_COOKIE_NAME=COOKIE_NAME,
|
||||
CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=COOKIE_DOMAIN,
|
||||
CORS_ALLOW_INSECURE=True
|
||||
)
|
||||
def test_skip_if_no_referer_insecure(self):
|
||||
response = self._get_response(delete_referer=True)
|
||||
self._assert_cookie_sent(response, False)
|
||||
|
||||
@override_settings(
|
||||
CROSS_DOMAIN_CSRF_COOKIE_NAME=COOKIE_NAME,
|
||||
CROSS_DOMAIN_CSRF_COOKIE_DOMAIN=COOKIE_DOMAIN,
|
||||
CORS_ORIGIN_WHITELIST=['www.example.com']
|
||||
)
|
||||
def test_set_cross_domain_cookie(self):
|
||||
response = self._get_response()
|
||||
self._assert_cookie_sent(response, True)
|
||||
|
||||
def _get_response(self,
|
||||
is_secure=True,
|
||||
csrf_cookie_used=True,
|
||||
cross_domain_decorator=True,
|
||||
referer=None,
|
||||
host=None,
|
||||
delete_referer=False):
|
||||
"""Process a request using the middleware. """
|
||||
request = Mock()
|
||||
request.META = {
|
||||
'HTTP_REFERER': (
|
||||
referer if referer is not None
|
||||
else self.REFERER
|
||||
)
|
||||
}
|
||||
request.is_secure = lambda: is_secure
|
||||
|
||||
if host is not None:
|
||||
request.get_host = lambda: host
|
||||
|
||||
if delete_referer:
|
||||
del request.META['HTTP_REFERER']
|
||||
|
||||
if csrf_cookie_used:
|
||||
request.META['CSRF_COOKIE_USED'] = True
|
||||
request.META['CSRF_COOKIE'] = self.COOKIE_VALUE
|
||||
|
||||
if cross_domain_decorator:
|
||||
request.META['CROSS_DOMAIN_CSRF_COOKIE_USED'] = True
|
||||
|
||||
return self.middleware.process_response(request, HttpResponse())
|
||||
|
||||
def _assert_cookie_sent(self, response, is_set):
|
||||
"""Check that the cross-domain CSRF cookie was sent. """
|
||||
if is_set:
|
||||
self.assertIn(self.COOKIE_NAME, response.cookies)
|
||||
cookie_header = str(response.cookies[self.COOKIE_NAME])
|
||||
|
||||
expected = 'Set-Cookie: {name}={value}; Domain={domain};'.format(
|
||||
name=self.COOKIE_NAME,
|
||||
value=self.COOKIE_VALUE,
|
||||
domain=self.COOKIE_DOMAIN
|
||||
)
|
||||
self.assertIn(expected, cookie_header)
|
||||
self.assertIn('Max-Age=31449600; Path=/; secure', cookie_header)
|
||||
|
||||
else:
|
||||
self.assertNotIn(self.COOKIE_NAME, response.cookies)
|
||||
@@ -4,25 +4,24 @@ consist primarily of authentication, request validation, and serialization.
|
||||
|
||||
"""
|
||||
from ipware.ip import get_ip
|
||||
from django.conf import settings
|
||||
from django.utils.decorators import method_decorator
|
||||
from opaque_keys import InvalidKeyError
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from openedx.core.djangoapps.user_api import api as user_api
|
||||
from openedx.core.lib.api.permissions import ApiKeyHeaderPermission, ApiKeyHeaderPermissionIsAuthenticated
|
||||
from rest_framework import status
|
||||
from rest_framework import permissions
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.throttling import UserRateThrottle
|
||||
from rest_framework.views import APIView
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys import InvalidKeyError
|
||||
from enrollment import api
|
||||
from enrollment.errors import (
|
||||
CourseNotFoundError, CourseEnrollmentError, CourseModeNotFoundError, CourseEnrollmentExistsError
|
||||
)
|
||||
from embargo import api as embargo_api
|
||||
from cors_csrf.decorators import ensure_csrf_cookie_cross_domain
|
||||
from util.authentication import SessionAuthenticationAllowInactiveUser, OAuth2AuthenticationAllowInactiveUser
|
||||
from util.disable_rate_limit import can_disable_rate_limit
|
||||
from enrollment import api
|
||||
from enrollment.errors import (
|
||||
CourseNotFoundError, CourseEnrollmentError,
|
||||
CourseModeNotFoundError, CourseEnrollmentExistsError
|
||||
)
|
||||
|
||||
|
||||
class EnrollmentUserThrottle(UserRateThrottle):
|
||||
@@ -96,6 +95,10 @@ class EnrollmentView(APIView, ApiKeyPermissionMixIn):
|
||||
permission_classes = ApiKeyHeaderPermissionIsAuthenticated,
|
||||
throttle_classes = EnrollmentUserThrottle,
|
||||
|
||||
# Since the course about page on the marketing site
|
||||
# uses this API to auto-enroll users, we need to support
|
||||
# cross-domain CSRF.
|
||||
@method_decorator(ensure_csrf_cookie_cross_domain)
|
||||
def get(self, request, course_id=None, user=None):
|
||||
"""Create, read, or update enrollment information for a user.
|
||||
|
||||
@@ -268,6 +271,10 @@ class EnrollmentListView(APIView, ApiKeyPermissionMixIn):
|
||||
permission_classes = ApiKeyHeaderPermissionIsAuthenticated,
|
||||
throttle_classes = EnrollmentUserThrottle,
|
||||
|
||||
# Since the course about page on the marketing site
|
||||
# uses this API to auto-enroll users, we need to support
|
||||
# cross-domain CSRF.
|
||||
@method_decorator(ensure_csrf_cookie_cross_domain)
|
||||
def get(self, request):
|
||||
"""
|
||||
Gets a list of all course enrollments for the currently logged in user.
|
||||
|
||||
Reference in New Issue
Block a user