Permalink
Browse files

csrf protection for django without cookies

  • Loading branch information...
0 parents commit 410ebb6ba759c4db5ee075558e3d8e1789bd411f Jeff Balogh committed Apr 19, 2011
Showing with 398 additions and 0 deletions.
  1. +27 −0 LICENSE
  2. +2 −0 MANIFEST.in
  3. +59 −0 README.rst
  4. +3 −0 requirements.txt
  5. +46 −0 runtests.sh
  6. +73 −0 session_csrf/__init__.py
  7. 0 session_csrf/models.py
  8. +157 −0 session_csrf/tests.py
  9. +31 −0 setup.py
27 LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2011, Mozilla Foundation.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ 1. Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ 3. Neither the name of django-csrf nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,2 @@
+include LICENSE
+include README.rst
@@ -0,0 +1,59 @@
+What is this?
+-------------
+
+``django-session-csrf`` is an alternative implementation of Django's CSRF
+protection that does not use cookies. Instead, it maintains the CSRF token on
+the server using Django's session backend. The csrf token must still be
+included in all POST requests (either with `csrfmiddlewaretoken` in the form or
+with the `X-CSRFTOKEN` header).
+
+
+Installation
+------------
+
+From PyPI::
+
+ pip install django-session-csrf
+
+From github::
+
+ git clone git://github.com/mozilla/django-session-csrf.git
+
+Replace ``django.core.context_processors.csrf`` with
+``session_csrf.context_processor`` in your ``TEMPLATE_CONTEXT_PROCESSORS``::
+
+ TEMPLATE_CONTEXT_PROCESSORS = (
+ ...
+ 'session_csrf.context_processor',
+ ...
+ )
+
+Replace ``django.middleware.csrf.CsrfViewMiddleware`` with
+``session_csrf.CsrfMiddleware`` in your ``MIDDLEWARE_CLASSES``::
+
+ MIDDLEWARE_CLASSES = (
+ ...
+ 'session_csrf.CsrfMiddleware',
+ ...
+ )
+
+Everything else should be identical to the built-in CSRF protection.
+
+
+Why do I want this?
+-------------------
+
+1. Your site is on a subdomain with other sites that are not under your
+ control, so cookies could come from anywhere.
+2. You're worried about attackers using Flash to forge HTTP headers.
+3. You're tired of requiring a Referer header.
+
+
+Why don't I want this?
+----------------------
+
+1. Storing tokens in sessions means you have to hit your session store more
+ often.
+2. You want CSRF protection for anonymous users. ``django-session-csrf`` does
+ not create CSRF tokens for anonymous users since we're worried about the
+ scalability of that.
@@ -0,0 +1,3 @@
+# Install these to run the tests:
+django
+mock
@@ -0,0 +1,46 @@
+#!/bin/sh
+
+SETTINGS='settings.py'
+
+cat > $SETTINGS <<EOF
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': 'test.db',
+ },
+}
+
+MIDDLEWARE_CLASSES = (
+ 'django.middleware.common.CommonMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'session_csrf.CsrfMiddleware',
+)
+
+TEMPLATE_CONTEXT_PROCESSORS = (
+ 'django.contrib.auth.context_processors.auth',
+ 'django.core.context_processors.debug',
+ 'django.core.context_processors.i18n',
+ 'django.core.context_processors.media',
+ 'django.core.context_processors.static',
+ 'django.contrib.messages.context_processors.messages',
+ 'session_csrf.context_processor',
+)
+
+ROOT_URLCONF = 'session_csrf.tests'
+
+INSTALLED_APPS = (
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'session_csrf',
+)
+EOF
+
+export PYTHONPATH=.
+export DJANGO_SETTINGS_MODULE=settings
+
+django-admin.py test session_csrf
+
+rm -f $SETTINGS*
+rm -f test.db
@@ -0,0 +1,73 @@
+"""CSRF protection without cookies."""
+import django.core.context_processors
+from django.middleware import csrf as django_csrf
+from django.utils import crypto
+
+
+# This overrides django.core.context_processors.csrf to dump our csrf_token
+# into the template context.
+def context_processor(request):
+ # Django warns about an empty token unless you call it NOTPROVIDED.
+ return {'csrf_token': getattr(request, 'csrf_token', 'NOTPROVIDED')}
+
+
+class CsrfMiddleware(object):
+
+ # csrf_processing_done prevents checking CSRF more than once. That could
+ # happen if the requires_csrf_token decorator is used.
+ def _accept(self, request):
+ request.csrf_processing_done = True
+
+ def _reject(self, request, reason):
+ return django_csrf._get_failure_view()(request, reason)
+
+ def process_request(self, request):
+ """
+ Add a CSRF token to the session for logged-in users.
+
+ The token is available at request.csrf_token.
+ """
+ if request.user.is_authenticated():
+ if 'csrf_token' not in request.session:
+ token = django_csrf._get_new_csrf_key()
+ request.csrf_token = request.session['csrf_token'] = token
+ else:
+ request.csrf_token = request.session['csrf_token']
+ else:
+ request.csrf_token = ''
+
+ def process_view(self, request, view_func, args, kwargs):
+ """Check the CSRF token if this is a POST."""
+ if getattr(request, 'csrf_processing_done', False):
+ return
+
+ # Allow @csrf_exempt views.
+ if getattr(view_func, 'csrf_exempt', False):
+ return
+
+ # Bail if this isn't a POST.
+ if request.method != 'POST':
+ return self._accept(request)
+
+ # The test client uses this to get around CSRF processing.
+ if getattr(request, '_dont_enforce_csrf_checks', False):
+ return self._accept(request)
+
+ # Try to get the token from the POST and fall back to looking at the
+ # X-CSRFTOKEN header.
+ user_token = request.POST.get('csrfmiddlewaretoken', '')
+ if user_token == '':
+ user_token = request.META.get('HTTP_X_CSRFTOKEN', '')
+
+ request_token = getattr(request, 'csrf_token', '')
+
+ # Check that both strings aren't empty and then check for a match.
+ if not ((user_token or request_token)
+ and crypto.constant_time_compare(user_token, request_token)):
+ reason = django_csrf.REASON_BAD_TOKEN
+ django_csrf.logger.warning(
+ 'Forbidden (%s): %s' % (reason, request.path),
+ extra=dict(status_code=403, request=request))
+ return self._reject(request, reason)
+ else:
+ return self._accept(request)
No changes.
@@ -0,0 +1,157 @@
+from collections import namedtuple
+
+import django.test
+from django import http
+from django.conf.urls.defaults import patterns
+from django.contrib.auth.models import User
+from django.core.handlers.wsgi import WSGIRequest
+from django.db import close_connection
+from django.shortcuts import render
+from django.template import context
+
+import mock
+
+from session_csrf import CsrfMiddleware
+
+
+urlpatterns = patterns('', ('^$', lambda r: http.HttpResponse()))
+
+
+class TestCsrfToken(django.test.TestCase):
+ urls = 'session_csrf.tests'
+
+ def setUp(self):
+ self.client.handler = ClientHandler()
+ User.objects.create_user('jbalogh', 'j@moz.com', 'password')
+
+ def login(self):
+ assert self.client.login(username='jbalogh', password='password')
+
+ def test_csrftoken_unauthenticated(self):
+ # request.csrf_token is '' for anonymous users.
+ response = self.client.get('/', follow=True)
+ self.assertEqual(response._request.csrf_token, '')
+
+ def test_csrftoken_authenticated(self):
+ # request.csrf_token is a random non-empty string for authed users.
+ self.login()
+ response = self.client.get('/', follow=True)
+ # The CSRF token is a 32-character MD5 string.
+ self.assertEqual(len(response._request.csrf_token), 32)
+
+ def test_csrftoken_new_session(self):
+ # The csrf_token is added to request.session the first time.
+ self.login()
+ response = self.client.get('/', follow=True)
+ # The CSRF token is a 32-character MD5 string.
+ token = response._request.session['csrf_token']
+ self.assertEqual(len(token), 32)
+ self.assertEqual(token, response._request.csrf_token)
+
+ def test_csrftoken_existing_session(self):
+ # The csrf_token in request.session is reused on subsequent requests.
+ self.login()
+ r1 = self.client.get('/', follow=True)
+ token = r1._request.session['csrf_token']
+
+ r2 = self.client.get('/', follow=True)
+ self.assertEqual(r1._request.csrf_token, r2._request.csrf_token)
+ self.assertEqual(token, r2._request.csrf_token)
+
+
+class TestCsrfMiddleware(django.test.TestCase):
+
+ def setUp(self):
+ self.token = 'a' * 32
+ self.rf = django.test.RequestFactory()
+ self.mw = CsrfMiddleware()
+
+ def process_view(self, request, view=None):
+ return self.mw.process_view(request, view, None, None)
+
+ def test_reject_view(self):
+ # Check that the reject view returns a 403.
+ response = self.process_view(self.rf.post('/'))
+ self.assertEqual(response.status_code, 403)
+
+ def test_csrf_exempt(self):
+ # Make sure @csrf_exempt still works.
+ view = namedtuple('_', 'csrf_exempt')
+ self.assertEqual(self.process_view(self.rf.post('/'), view), None)
+
+ def test_only_check_post(self):
+ # CSRF should only get checked on POST requests.
+ self.assertEqual(self.process_view(self.rf.get('/')), None)
+
+ def test_csrfmiddlewaretoken(self):
+ # The user token should be found in POST['csrfmiddlewaretoken'].
+ request = self.rf.post('/', {'csrfmiddlewaretoken': self.token})
+ self.assertEqual(self.process_view(request).status_code, 403)
+
+ request.csrf_token = self.token
+ self.assertEqual(self.process_view(request), None)
+
+ def test_x_csrftoken(self):
+ # The user token can be found in the X-CSRFTOKEN header.
+ request = self.rf.post('/', HTTP_X_CSRFTOKEN=self.token)
+ self.assertEqual(self.process_view(request).status_code, 403)
+
+ request.csrf_token = self.token
+ self.assertEqual(self.process_view(request), None)
+
+ def test_require_request_token_or_user_token(self):
+ # Blank request and user tokens raise an error on POST.
+ request = self.rf.post('/', HTTP_X_CSRFTOKEN='')
+ request.csrf_token = ''
+ self.assertEqual(self.process_view(request).status_code, 403)
+
+ def test_token_no_match(self):
+ # A 403 is returned when the tokens don't match.
+ request = self.rf.post('/', HTTP_X_CSRFTOKEN='woo')
+ request.csrf_token = ''
+ self.assertEqual(self.process_view(request).status_code, 403)
+
+ def test_csrf_token_context_processor(self):
+ # Our CSRF token should be available in the template context.
+ request = mock.Mock()
+ request.csrf_token = self.token
+ request.groups = []
+ ctx = {}
+ for processor in context.get_standard_processors():
+ ctx.update(processor(request))
+ self.assertEqual(ctx['csrf_token'], self.token)
+
+
+class ClientHandler(django.test.client.ClientHandler):
+ """
+ Handler that stores the real request object on the response.
+
+ Almost all the code comes from the parent class.
+ """
+
+ def __call__(self, environ):
+ from django.conf import settings
+ from django.core import signals
+
+ # Set up middleware if needed. We couldn't do this earlier, because
+ # settings weren't available.
+ if self._request_middleware is None:
+ self.load_middleware()
+
+ signals.request_started.send(sender=self.__class__)
+ try:
+ request = WSGIRequest(environ)
+ # sneaky little hack so that we can easily get round
+ # CsrfViewMiddleware. This makes life easier, and is probably
+ # required for backwards compatibility with external tests against
+ # admin views.
+ request._dont_enforce_csrf_checks = not self.enforce_csrf_checks
+ response = self.get_response(request)
+ finally:
+ signals.request_finished.disconnect(close_connection)
+ signals.request_finished.send(sender=self.__class__)
+ signals.request_finished.connect(close_connection)
+
+ # Store the request object.
+ response._request = request
+ return response
Oops, something went wrong.

0 comments on commit 410ebb6

Please sign in to comment.