Commit 05c85747 by Xavier Antoviaque

xblock-external-ui: Adds support for CORS headers (cross-domain request)

xblock-external-ui: Alternate referer check for CORS requests
xblock-external-ui: Allow to disable httponly on session cookies
xblock-external-ui: Add a unit test for CorsCSRFMiddleware
parent 2ba49579
......@@ -144,6 +144,7 @@ if 'loc_cache' not in CACHES:
}
SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
SESSION_COOKIE_HTTPONLY = ENV_TOKENS.get('SESSION_COOKIE_HTTPONLY', True)
SESSION_ENGINE = ENV_TOKENS.get('SESSION_ENGINE', SESSION_ENGINE)
SESSION_COOKIE_SECURE = ENV_TOKENS.get('SESSION_COOKIE_SECURE', SESSION_COOKIE_SECURE)
......
"""
Middleware for handling CSRF checks with CORS requests
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,
with a man in the middle on the HTTP requests.
https://github.com/django/django/blob/b91c385e324f1cb94d20e2ad146372c259d51d3b/django/middleware/csrf.py#L117
This doesn't work well with CORS requests, which aren't vulnerable to this attack when
the server from which the request is coming uses HTTPS too, as it prevents the man in the
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.
"""
import logging
import urlparse
from django.conf import settings
from django.middleware.csrf import CsrfViewMiddleware
log = logging.getLogger(__name__)
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
"""
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
def process_view(self, request, callback, callback_args, callback_kwargs):
if not self.is_enabled(request):
return
is_secure_default = request.is_secure
def is_secure_patched():
"""
Avoid triggering the additional CSRF middleware checks on the referrer
"""
return False
request.is_secure = is_secure_patched
res = super(CorsCSRFMiddleware, self).process_view(request, callback, callback_args, callback_kwargs)
request.is_secure = is_secure_default
return res
"""
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)
......@@ -960,7 +960,7 @@ def xblock_view(request, course_id, usage_id, view_name):
if not request.user.is_authenticated():
raise PermissionDenied
instance, tracking_context = _get_module_by_usage_id(request, course_id, usage_id)
instance, _ = _get_module_by_usage_id(request, course_id, usage_id)
try:
fragment = instance.render(view_name, context=request.GET)
......
......@@ -151,6 +151,7 @@ SITE_NAME = ENV_TOKENS['SITE_NAME']
HTTPS = ENV_TOKENS.get('HTTPS', HTTPS)
SESSION_ENGINE = ENV_TOKENS.get('SESSION_ENGINE', SESSION_ENGINE)
SESSION_COOKIE_DOMAIN = ENV_TOKENS.get('SESSION_COOKIE_DOMAIN')
SESSION_COOKIE_HTTPONLY = ENV_TOKENS.get('SESSION_COOKIE_HTTPONLY', True)
REGISTRATION_EXTRA_FIELDS = ENV_TOKENS.get('REGISTRATION_EXTRA_FIELDS', REGISTRATION_EXTRA_FIELDS)
SESSION_COOKIE_SECURE = ENV_TOKENS.get('SESSION_COOKIE_SECURE', SESSION_COOKIE_SECURE)
......@@ -302,6 +303,17 @@ if FEATURES.get('AUTH_USE_CAS'):
# Example: {'CN': 'http://api.xuetangx.com/edx/video?s3_url='}
VIDEO_CDN_URL = ENV_TOKENS.get('VIDEO_CDN_URL', {})
############# CORS headers for cross-domain requests #################
if FEATURES.get('ENABLE_CORS_HEADERS'):
INSTALLED_APPS += ('corsheaders', 'cors_csrf')
MIDDLEWARE_CLASSES = (
'corsheaders.middleware.CorsMiddleware',
'cors_csrf.middleware.CorsCSRFMiddleware',
) + MIDDLEWARE_CLASSES
CORS_ALLOW_CREDENTIALS = True
CORS_ORIGIN_WHITELIST = ENV_TOKENS.get('CORS_ORIGIN_WHITELIST', ())
############################## SECURE AUTH ITEMS ###############
# Secret things: passwords, access keys, etc.
......
......@@ -1685,6 +1685,16 @@ if FEATURES.get('AUTH_USE_CAS'):
INSTALLED_APPS += ('django_cas',)
MIDDLEWARE_CLASSES += ('django_cas.middleware.CASMiddleware',)
############# CORS headers for cross-domain requests #################
if FEATURES.get('ENABLE_CORS_HEADERS'):
INSTALLED_APPS += ('corsheaders', 'cors_csrf')
MIDDLEWARE_CLASSES = (
'corsheaders.middleware.CorsMiddleware',
'cors_csrf.middleware.CorsCSRFMiddleware',
) + MIDDLEWARE_CLASSES
CORS_ALLOW_CREDENTIALS = True
CORS_ORIGIN_WHITELIST = ()
###################### Registration ##################################
......
......@@ -115,6 +115,9 @@ transifex-client==0.10
# Ip network support for Embargo feature
ipaddr==2.1.11
# Used to allow to configure CORS headers for cross-domain requests
django-cors-headers==0.13
# We've tried several times to update the debug toolbar to version 1.0.1,
# and had problems each time, resulting in us rolling back to 0.9.4. Before
# submitting another pull request to do this update, check the following:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment