├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── contribute.json ├── requirements.txt ├── runtests.sh ├── session_csrf ├── __init__.py ├── models.py └── tests.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | *.sw[po] 3 | *~ 4 | build/ 5 | dist/ 6 | django_session_csrf.egg-info/ 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | sudo: false 3 | python: 4 | - "2.7" 5 | - "3.4" 6 | - "3.5" 7 | 8 | env: 9 | - DJANGO="Django==1.8.15" 10 | - DJANGO="Django==1.9.10" 11 | - DJANGO="Django==1.10.1" 12 | 13 | matrix: 14 | include: 15 | # include Python 3.3 for Django 1.8 only 16 | # remove once Django 1.8 is no longer supported 17 | - python: "3.3" 18 | env: DJANGO="Django==1.8.15" 19 | 20 | install: pip install $DJANGO 21 | 22 | script: ./runtests.sh 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Mozilla Foundation. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of django-csrf nor the names of its contributors 15 | may be used to endorse or promote products derived from this software 16 | without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | What is this? 2 | ------------- 3 | 4 | ``django-session-csrf`` is an alternative implementation of Django's CSRF 5 | protection that does not use cookies. Instead, it maintains the CSRF token on 6 | the server using Django's session backend. The csrf token must still be 7 | included in all POST requests (either with `csrfmiddlewaretoken` in the form or 8 | with the `X-CSRFTOKEN` header). 9 | 10 | 11 | Installation 12 | ------------ 13 | 14 | From PyPI:: 15 | 16 | pip install django-session-csrf 17 | 18 | From github:: 19 | 20 | git clone git://github.com/mozilla/django-session-csrf.git 21 | 22 | Replace ``django.core.context_processors.csrf`` with 23 | ``session_csrf.context_processor`` in your ``TEMPLATE_CONTEXT_PROCESSORS``:: 24 | 25 | TEMPLATE_CONTEXT_PROCESSORS = ( 26 | ... 27 | 'session_csrf.context_processor', 28 | ... 29 | ) 30 | 31 | Replace ``django.middleware.csrf.CsrfViewMiddleware`` with 32 | ``session_csrf.CsrfMiddleware`` in your ``MIDDLEWARE_CLASSES`` 33 | and make sure it is listed after the AuthenticationMiddleware:: 34 | 35 | MIDDLEWARE_CLASSES = ( 36 | ... 37 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 38 | ... 39 | 'session_csrf.CsrfMiddleware', 40 | ... 41 | ) 42 | 43 | Then we have to monkeypatch Django to fix the ``@csrf_protect`` decorator:: 44 | 45 | import session_csrf 46 | session_csrf.monkeypatch() 47 | 48 | Make sure that's in something like your root ``urls.py`` so the patch gets 49 | applied before your views are imported. 50 | 51 | 52 | Differences from Django 53 | ----------------------- 54 | 55 | ``django-session-csrf`` does not assign CSRF tokens to anonymous users because 56 | we don't want to support a session for every anonymous user. Instead, views 57 | that need anonymous forms can be decorated with ``@anonymous_csrf``:: 58 | 59 | from session_csrf import anonymous_csrf 60 | 61 | @anonymous_csrf 62 | def login(request): 63 | ... 64 | 65 | ``anonymous_csrf`` uses the cache to give anonymous users a lightweight 66 | session. It sends a cookie to uniquely identify the user and stores the CSRF 67 | token in the cache. It can be controlled through these settings: 68 | 69 | ``ANON_COOKIE`` 70 | the name used for the anonymous user's cookie 71 | 72 | Default: ``anoncsrf`` 73 | 74 | ``ANON_TIMEOUT`` 75 | the cache timeout (in seconds) to use for the anonymous CSRF tokens 76 | 77 | Default: ``60 * 60 * 2 # 2 hours`` 78 | 79 | Note that by default Django uses local-memory caching, which will not 80 | work with anonymous CSRF if there is more than one web server thread. 81 | To use anonymous CSRF, you must configure a cache that's shared 82 | between web server instances, such as Memcached. See the `Django cache 83 | documentation `_ 84 | for more information. 85 | 86 | 87 | If you only want a view to have CSRF protection for logged-in users, you can 88 | use the ``anonymous_csrf_exempt`` decorator. This could be useful if the 89 | anonymous view is protected through a CAPTCHA, for example. 90 | 91 | :: 92 | 93 | from session_csrf import anonymous_csrf_exempt 94 | 95 | @anonymous_csrf_exempt 96 | def protected_in_another_way(request): 97 | ... 98 | 99 | 100 | If you want all views to have CSRF protection for anonymous users as Django 101 | does, use the following setting: 102 | 103 | ``ANON_ALWAYS`` 104 | always provide CSRF protection for anonymous users 105 | 106 | Default: False 107 | 108 | 109 | Why do I want this? 110 | ------------------- 111 | 112 | 1. Your site is on a subdomain with other sites that are not under your 113 | control, so cookies could come from anywhere. 114 | 2. You're worried about attackers using Flash to forge HTTP headers. 115 | 3. You're tired of requiring a Referer header. 116 | 117 | 118 | Why don't I want this? 119 | ---------------------- 120 | 121 | 1. Storing tokens in sessions means you have to hit your session store more 122 | often. 123 | 2. It's a little bit more work to CSRF-protect forms for anonymous users. 124 | -------------------------------------------------------------------------------- /contribute.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "django-session-csrf", 3 | "description": "CSRF protection for Django without cookies.", 4 | "repository": { 5 | "url": "https://github.com/mozilla/django-session-csrf", 6 | "license": "BSD 3-Clause" 7 | }, 8 | "participate": { 9 | "home": "https://github.com/mozilla/django-session-csrf", 10 | "docs": "https://github.com/mozilla/django-session-csrf" 11 | }, 12 | "bugs": { 13 | "list": "https://github.com/mozilla/django-session-csrf/issues", 14 | "report": "https://github.com/mozilla/django-session-csrf/issues/new" 15 | }, 16 | "keywords": [ 17 | "django", 18 | "python", 19 | "csrf" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Install these to run the tests: 2 | django 3 | mock 4 | -------------------------------------------------------------------------------- /runtests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | SETTINGS='settings.py' 4 | 5 | cat > $SETTINGS <= (1, 10, 0) else object): 56 | 57 | # csrf_processing_done prevents checking CSRF more than once. That could 58 | # happen if the requires_csrf_token decorator is used. 59 | def _accept(self, request): 60 | request.csrf_processing_done = True 61 | 62 | def _reject(self, request, reason): 63 | return django_csrf._get_failure_view()(request, reason) 64 | 65 | def process_request(self, request): 66 | """ 67 | Add a CSRF token to the session for logged-in users. 68 | 69 | The token is available at request.csrf_token. 70 | """ 71 | if hasattr(request, 'csrf_token'): 72 | return 73 | if is_user_authenticated(request): 74 | if 'csrf_token' not in request.session: 75 | token = django_get_new_csrf_string() 76 | request.csrf_token = request.session['csrf_token'] = token 77 | else: 78 | request.csrf_token = request.session['csrf_token'] 79 | else: 80 | key = None 81 | token = '' 82 | if ANON_COOKIE in request.COOKIES: 83 | key = request.COOKIES[ANON_COOKIE] 84 | token = cache.get(prep_key(key), '') 85 | if ANON_ALWAYS: 86 | # pretend that anonymous_csrf was applied to the view 87 | if not key: 88 | key = django_get_new_csrf_string() 89 | if not token: 90 | token = django_get_new_csrf_string() 91 | request._anon_csrf_key = key 92 | cache.set(prep_key(key), token, ANON_TIMEOUT) 93 | request.csrf_token = token 94 | 95 | def process_view(self, request, view_func, args, kwargs): 96 | """Check the CSRF token if this is a POST.""" 97 | if getattr(request, 'csrf_processing_done', False): 98 | return 99 | 100 | # Allow @csrf_exempt views. 101 | if getattr(view_func, 'csrf_exempt', False): 102 | return 103 | 104 | if (getattr(view_func, 'anonymous_csrf_exempt', False) 105 | and not is_user_authenticated(request)): 106 | return 107 | 108 | # Bail if this is a safe method. 109 | if request.method in ('GET', 'HEAD', 'OPTIONS', 'TRACE'): 110 | return self._accept(request) 111 | 112 | # The test client uses this to get around CSRF processing. 113 | if getattr(request, '_dont_enforce_csrf_checks', False): 114 | return self._accept(request) 115 | 116 | # Try to get the token from the POST and fall back to looking at the 117 | # X-CSRFTOKEN header. 118 | user_token = request.POST.get('csrfmiddlewaretoken', '') 119 | if user_token == '': 120 | user_token = request.META.get('HTTP_X_CSRFTOKEN', '') 121 | 122 | request_token = getattr(request, 'csrf_token', '') 123 | 124 | # Check that both strings aren't empty and then check for a match. 125 | if not ((user_token or request_token) 126 | and crypto.constant_time_compare(user_token, request_token)): 127 | reason = django_csrf.REASON_BAD_TOKEN 128 | django_csrf.logger.warning( 129 | 'Forbidden (%s): %s' % (reason, request.path), 130 | extra=dict(status_code=403, request=request)) 131 | return self._reject(request, reason) 132 | else: 133 | return self._accept(request) 134 | 135 | def process_response(self, request, response): 136 | if hasattr(request, '_anon_csrf_key'): 137 | # Set or reset the cache and cookie timeouts. 138 | response.set_cookie(ANON_COOKIE, request._anon_csrf_key, 139 | max_age=ANON_TIMEOUT, httponly=True, 140 | secure=request.is_secure()) 141 | patch_vary_headers(response, ['Cookie']) 142 | return response 143 | 144 | 145 | def anonymous_csrf(f): 146 | """Decorator that assigns a CSRF token to an anonymous user.""" 147 | @functools.wraps(f) 148 | def wrapper(request, *args, **kw): 149 | use_anon_cookie = not (is_user_authenticated(request) or ANON_ALWAYS) 150 | if use_anon_cookie: 151 | if ANON_COOKIE in request.COOKIES: 152 | key = request.COOKIES[ANON_COOKIE] 153 | token = cache.get(prep_key(key)) or django_get_new_csrf_string() 154 | else: 155 | key = django_get_new_csrf_string() 156 | token = django_get_new_csrf_string() 157 | cache.set(prep_key(key), token, ANON_TIMEOUT) 158 | request.csrf_token = token 159 | response = f(request, *args, **kw) 160 | if use_anon_cookie: 161 | # Set or reset the cache and cookie timeouts. 162 | response.set_cookie(ANON_COOKIE, key, max_age=ANON_TIMEOUT, 163 | httponly=True, secure=request.is_secure()) 164 | patch_vary_headers(response, ['Cookie']) 165 | return response 166 | return wrapper 167 | 168 | 169 | def anonymous_csrf_exempt(f): 170 | """Like @csrf_exempt but only for anonymous requests.""" 171 | f.anonymous_csrf_exempt = True 172 | return f 173 | 174 | 175 | # Replace Django's middleware with our own. 176 | def monkeypatch(): 177 | from django.views.decorators import csrf as csrf_dec 178 | django_csrf.CsrfViewMiddleware = CsrfMiddleware 179 | csrf_dec.csrf_protect = csrf_dec.decorator_from_middleware(CsrfMiddleware) 180 | -------------------------------------------------------------------------------- /session_csrf/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mozilla/django-session-csrf/bb6ce72802d0a54d028ae891df3d2268501b6c1a/session_csrf/models.py -------------------------------------------------------------------------------- /session_csrf/tests.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | import django.test 4 | from django import http 5 | from django.conf.urls import url 6 | from django.contrib.auth import logout 7 | from django.contrib.auth.middleware import AuthenticationMiddleware 8 | from django.contrib.auth.models import User 9 | from django.contrib.sessions.middleware import SessionMiddleware 10 | from django.contrib.sessions.models import Session 11 | from django.core import signals 12 | from django.core.cache import cache 13 | from django.core.handlers.wsgi import WSGIRequest 14 | from django.core.exceptions import ImproperlyConfigured 15 | 16 | import session_csrf 17 | from session_csrf import (anonymous_csrf, anonymous_csrf_exempt, 18 | CsrfMiddleware, prep_key) 19 | try: 20 | from unittest import mock 21 | except ImportError: 22 | # Python 2.7 doesn't have unittest.mock, but it is available on PyPi 23 | import mock 24 | 25 | 26 | urlpatterns = [ 27 | url('^$', lambda r: http.HttpResponse()), 28 | url('^anon$', anonymous_csrf(lambda r: http.HttpResponse())), 29 | url('^no-anon-csrf$', anonymous_csrf_exempt(lambda r: http.HttpResponse())), 30 | url('^logout$', anonymous_csrf(lambda r: logout(r) or http.HttpResponse())), 31 | ] 32 | 33 | 34 | class TestCsrfToken(django.test.TestCase): 35 | 36 | def setUp(self): 37 | self.client.handler = ClientHandler() 38 | User.objects.create_user('jbalogh', 'j@moz.com', 'password') 39 | self.save_ANON_ALWAYS = session_csrf.ANON_ALWAYS 40 | session_csrf.ANON_ALWAYS = False 41 | 42 | def tearDown(self): 43 | session_csrf.ANON_ALWAYS = self.save_ANON_ALWAYS 44 | 45 | def login(self): 46 | assert self.client.login(username='jbalogh', password='password') 47 | 48 | def test_csrftoken_unauthenticated(self): 49 | # request.csrf_token is '' for anonymous users. 50 | response = self.client.get('/', follow=True) 51 | self.assertEqual(response.wsgi_request.csrf_token, '') 52 | 53 | def test_csrftoken_authenticated(self): 54 | # request.csrf_token is a random non-empty string for authed users. 55 | self.login() 56 | response = self.client.get('/', follow=True) 57 | # The CSRF token is a 32-character MD5 string. 58 | self.assertEqual(len(response.wsgi_request.csrf_token), 32) 59 | 60 | def test_csrftoken_new_session(self): 61 | # The csrf_token is added to request.session the first time. 62 | self.login() 63 | response = self.client.get('/', follow=True) 64 | # The CSRF token is a 32-character MD5 string. 65 | token = response.wsgi_request.session['csrf_token'] 66 | self.assertEqual(len(token), 32) 67 | self.assertEqual(token, response.wsgi_request.csrf_token) 68 | 69 | def test_csrftoken_existing_session(self): 70 | # The csrf_token in request.session is reused on subsequent requests. 71 | self.login() 72 | r1 = self.client.get('/', follow=True) 73 | token = r1.wsgi_request.session['csrf_token'] 74 | 75 | r2 = self.client.get('/', follow=True) 76 | self.assertEqual(r1.wsgi_request.csrf_token, r2.wsgi_request.csrf_token) 77 | self.assertEqual(token, r2.wsgi_request.csrf_token) 78 | 79 | 80 | class TestCsrfMiddleware(django.test.TestCase): 81 | 82 | def setUp(self): 83 | self.token = 'a' * 32 84 | self.rf = django.test.RequestFactory() 85 | self.mw = CsrfMiddleware() 86 | 87 | def process_view(self, request, view=None): 88 | return self.mw.process_view(request, view, None, None) 89 | 90 | def test_anon_token_from_cookie(self): 91 | rf = django.test.RequestFactory() 92 | rf.cookies['anoncsrf'] = self.token 93 | cache.set(prep_key(self.token), 'woo') 94 | request = rf.get('/') 95 | SessionMiddleware().process_request(request) 96 | AuthenticationMiddleware().process_request(request) 97 | self.mw.process_request(request) 98 | self.assertEqual(request.csrf_token, 'woo') 99 | 100 | def test_set_csrftoken_once(self): 101 | # Make sure process_request only sets request.csrf_token once. 102 | request = self.rf.get('/') 103 | request.csrf_token = 'woo' 104 | self.mw.process_request(request) 105 | self.assertEqual(request.csrf_token, 'woo') 106 | 107 | def test_reject_view(self): 108 | # Check that the reject view returns a 403. 109 | response = self.process_view(self.rf.post('/')) 110 | self.assertEqual(response.status_code, 403) 111 | 112 | def test_csrf_exempt(self): 113 | # Make sure @csrf_exempt still works. 114 | view = type(str(""), (), {'csrf_exempt': True})() 115 | self.assertEqual(self.process_view(self.rf.post('/'), view), None) 116 | 117 | def test_safe_whitelist(self): 118 | # CSRF should not get checked on these methods. 119 | self.assertEqual(self.process_view(self.rf.get('/')), None) 120 | self.assertEqual(self.process_view(self.rf.head('/')), None) 121 | self.assertEqual(self.process_view(self.rf.options('/')), None) 122 | 123 | def test_unsafe_methods(self): 124 | self.assertEqual(self.process_view(self.rf.post('/')).status_code, 125 | 403) 126 | self.assertEqual(self.process_view(self.rf.put('/')).status_code, 127 | 403) 128 | self.assertEqual(self.process_view(self.rf.delete('/')).status_code, 129 | 403) 130 | 131 | def test_csrfmiddlewaretoken(self): 132 | # The user token should be found in POST['csrfmiddlewaretoken']. 133 | request = self.rf.post('/', {'csrfmiddlewaretoken': self.token}) 134 | self.assertEqual(self.process_view(request).status_code, 403) 135 | 136 | request.csrf_token = self.token 137 | self.assertEqual(self.process_view(request), None) 138 | 139 | def test_x_csrftoken(self): 140 | # The user token can be found in the X-CSRFTOKEN header. 141 | request = self.rf.post('/', HTTP_X_CSRFTOKEN=self.token) 142 | self.assertEqual(self.process_view(request).status_code, 403) 143 | 144 | request.csrf_token = self.token 145 | self.assertEqual(self.process_view(request), None) 146 | 147 | def test_require_request_token_or_user_token(self): 148 | # Blank request and user tokens raise an error on POST. 149 | request = self.rf.post('/', HTTP_X_CSRFTOKEN='') 150 | request.csrf_token = '' 151 | self.assertEqual(self.process_view(request).status_code, 403) 152 | 153 | def test_token_no_match(self): 154 | # A 403 is returned when the tokens don't match. 155 | request = self.rf.post('/', HTTP_X_CSRFTOKEN='woo') 156 | request.csrf_token = '' 157 | self.assertEqual(self.process_view(request).status_code, 403) 158 | 159 | def test_csrf_token_context_processor(self): 160 | # Our CSRF token should be available in the template context. 161 | request = mock.Mock() 162 | request.csrf_token = self.token 163 | request.groups = [] 164 | ctx = {} 165 | for processor in get_context_processors(): 166 | ctx.update(processor(request)) 167 | self.assertEqual(ctx['csrf_token'], self.token) 168 | 169 | def test_process_view_without_authentication_middleware(self): 170 | # No request.user 171 | # Same as would happen if you never use the built-in 172 | # AuthenticationMiddleware. 173 | request = self.rf.get('/') 174 | self.assertEqual(self.mw.process_request(request), None) 175 | 176 | 177 | class TestAnonymousCsrf(django.test.TestCase): 178 | urls = 'session_csrf.tests' 179 | 180 | def setUp(self): 181 | self.token = 'a' * 32 182 | self.rf = django.test.RequestFactory() 183 | User.objects.create_user('jbalogh', 'j@moz.com', 'password') 184 | self.client.handler = ClientHandler(enforce_csrf_checks=True) 185 | self.save_ANON_ALWAYS = session_csrf.ANON_ALWAYS 186 | session_csrf.ANON_ALWAYS = False 187 | 188 | def tearDown(self): 189 | session_csrf.ANON_ALWAYS = self.save_ANON_ALWAYS 190 | 191 | def login(self): 192 | assert self.client.login(username='jbalogh', password='password') 193 | 194 | def test_authenticated_request(self): 195 | # Nothing special happens, nothing breaks. 196 | # Find the CSRF token in the session. 197 | self.login() 198 | response = self.client.get('/anon') 199 | sessionid = response.cookies['sessionid'].value 200 | session = Session.objects.get(session_key=sessionid) 201 | token = session.get_decoded()['csrf_token'] 202 | 203 | response = self.client.post('/anon', HTTP_X_CSRFTOKEN=token) 204 | self.assertEqual(response.status_code, 200) 205 | 206 | def test_unauthenticated_request(self): 207 | # We get a 403 since we're not sending a token. 208 | response = self.client.post('/anon') 209 | self.assertEqual(response.status_code, 403) 210 | 211 | def test_no_anon_cookie(self): 212 | # We don't get an anon cookie on non-@anonymous_csrf views. 213 | response = self.client.get('/') 214 | self.assertEqual(response.cookies, {}) 215 | 216 | def test_new_anon_token_on_request(self): 217 | # A new anon user gets a key+token on the request and response. 218 | response = self.client.get('/anon') 219 | # Get the key from the cookie and find the token in the cache. 220 | key = response.cookies['anoncsrf'].value 221 | self.assertEqual(response.wsgi_request.csrf_token, cache.get(prep_key(key))) 222 | 223 | def test_existing_anon_cookie_on_request(self): 224 | # We reuse an existing anon cookie key+token. 225 | response = self.client.get('/anon') 226 | key = response.cookies['anoncsrf'].value 227 | # Now check that subsequent requests use that cookie. 228 | response = self.client.get('/anon') 229 | self.assertEqual(response.cookies['anoncsrf'].value, key) 230 | self.assertEqual(response.wsgi_request.csrf_token, cache.get(prep_key(key))) 231 | 232 | def test_new_anon_token_on_response(self): 233 | # The anon cookie is sent and we vary on Cookie. 234 | response = self.client.get('/anon') 235 | self.assertIn('anoncsrf', response.cookies) 236 | self.assertEqual(response['Vary'], 'Cookie') 237 | 238 | def test_existing_anon_token_on_response(self): 239 | # The anon cookie is sent and we vary on Cookie, reusing the old value. 240 | response = self.client.get('/anon') 241 | key = response.cookies['anoncsrf'].value 242 | 243 | response = self.client.get('/anon') 244 | self.assertEqual(response.cookies['anoncsrf'].value, key) 245 | self.assertIn('anoncsrf', response.cookies) 246 | self.assertEqual(response['Vary'], 'Cookie') 247 | 248 | def test_anon_csrf_logout(self): 249 | # Beware of views that logout the user. 250 | self.login() 251 | response = self.client.get('/logout') 252 | self.assertEqual(response.status_code, 200) 253 | 254 | def test_existing_anon_cookie_not_in_cache(self): 255 | response = self.client.get('/anon') 256 | self.assertEqual(len(response.wsgi_request.csrf_token), 32) 257 | 258 | # Clear cache and make sure we still get a token 259 | cache.clear() 260 | response = self.client.get('/anon') 261 | self.assertEqual(len(response.wsgi_request.csrf_token), 32) 262 | 263 | def test_anonymous_csrf_exempt(self): 264 | response = self.client.post('/no-anon-csrf') 265 | self.assertEqual(response.status_code, 200) 266 | 267 | self.login() 268 | response = self.client.post('/no-anon-csrf') 269 | self.assertEqual(response.status_code, 403) 270 | 271 | 272 | class TestAnonAlways(django.test.TestCase): 273 | # Repeats some tests with ANON_ALWAYS = True 274 | urls = 'session_csrf.tests' 275 | 276 | def setUp(self): 277 | self.token = 'a' * 32 278 | self.rf = django.test.RequestFactory() 279 | User.objects.create_user('jbalogh', 'j@moz.com', 'password') 280 | self.client.handler = ClientHandler(enforce_csrf_checks=True) 281 | self.save_ANON_ALWAYS = session_csrf.ANON_ALWAYS 282 | session_csrf.ANON_ALWAYS = True 283 | 284 | def tearDown(self): 285 | session_csrf.ANON_ALWAYS = self.save_ANON_ALWAYS 286 | 287 | def login(self): 288 | assert self.client.login(username='jbalogh', password='password') 289 | 290 | def test_csrftoken_unauthenticated(self): 291 | # request.csrf_token is set for anonymous users 292 | # when ANON_ALWAYS is enabled. 293 | response = self.client.get('/', follow=True) 294 | # The CSRF token is a 32-character MD5 string. 295 | self.assertEqual(len(response.wsgi_request.csrf_token), 32) 296 | 297 | def test_authenticated_request(self): 298 | # Nothing special happens, nothing breaks. 299 | # Find the CSRF token in the session. 300 | self.login() 301 | response = self.client.get('/', follow=True) 302 | sessionid = response.cookies['sessionid'].value 303 | session = Session.objects.get(session_key=sessionid) 304 | token = session.get_decoded()['csrf_token'] 305 | 306 | response = self.client.post('/', follow=True, HTTP_X_CSRFTOKEN=token) 307 | self.assertEqual(response.status_code, 200) 308 | 309 | def test_unauthenticated_request(self): 310 | # We get a 403 since we're not sending a token. 311 | response = self.client.post('/') 312 | self.assertEqual(response.status_code, 403) 313 | 314 | def test_new_anon_token_on_request(self): 315 | # A new anon user gets a key+token on the request and response. 316 | response = self.client.get('/') 317 | # Get the key from the cookie and find the token in the cache. 318 | key = response.cookies['anoncsrf'].value 319 | self.assertEqual(response.wsgi_request.csrf_token, cache.get(prep_key(key))) 320 | 321 | def test_existing_anon_cookie_on_request(self): 322 | # We reuse an existing anon cookie key+token. 323 | response = self.client.get('/') 324 | key = response.cookies['anoncsrf'].value 325 | 326 | # Now check that subsequent requests use that cookie. 327 | response = self.client.get('/') 328 | self.assertEqual(response.cookies['anoncsrf'].value, key) 329 | self.assertEqual(response.wsgi_request.csrf_token, cache.get(prep_key(key))) 330 | self.assertEqual(response['Vary'], 'Cookie') 331 | 332 | def test_anon_csrf_logout(self): 333 | # Beware of views that logout the user. 334 | self.login() 335 | response = self.client.get('/logout') 336 | self.assertEqual(response.status_code, 200) 337 | 338 | def test_existing_anon_cookie_not_in_cache(self): 339 | response = self.client.get('/') 340 | self.assertEqual(len(response.wsgi_request.csrf_token), 32) 341 | 342 | # Clear cache and make sure we still get a token 343 | cache.clear() 344 | response = self.client.get('/') 345 | self.assertEqual(len(response.wsgi_request.csrf_token), 32) 346 | 347 | def test_massive_anon_cookie(self): 348 | # if the key + PREFIX + setting prefix is greater than 250 349 | # memcache will cry and you get a warning if you use LocMemCache 350 | junk = 'x' * 300 351 | with mock.patch('warnings.warn') as warner: 352 | response = self.client.get('/', HTTP_COOKIE='anoncsrf=%s' % junk) 353 | self.assertEqual(response.status_code, 200) 354 | self.assertEqual(warner.call_count, 0) 355 | 356 | def test_surprising_characters(self): 357 | c = 'anoncsrf="|dir; multidb_pin_writes=y; sessionid="gAJ9cQFVC' 358 | with mock.patch('warnings.warn') as warner: 359 | response = self.client.get('/', HTTP_COOKIE=c) 360 | self.assertEqual(response.status_code, 200) 361 | self.assertEqual(warner.call_count, 0) 362 | 363 | 364 | def get_context_processors(): 365 | """Get context processors in a way that works for Django 1.7 and 1.8+""" 366 | try: 367 | # 1.7 368 | from django.template.context import get_standard_processors 369 | return get_standard_processors() 370 | except ImportError: 371 | # 1.8+ 372 | try: 373 | from django.template.engine import Engine 374 | engine = Engine.get_default() 375 | except ImproperlyConfigured: 376 | return [] 377 | return engine.template_context_processors 378 | 379 | 380 | # for 1.7 support 381 | class ClientHandler(django.test.client.ClientHandler): 382 | @property 383 | def wsgi_request_middleware(self): 384 | return self._request_middleware 385 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | ROOT = os.path.abspath(os.path.dirname(__file__)) 5 | 6 | 7 | setup( 8 | name='django-session-csrf', 9 | version='0.7.1', 10 | description='CSRF protection for Django without cookies.', 11 | long_description=open(os.path.join(ROOT, 'README.rst')).read(), 12 | author='Jeff Balogh', 13 | author_email='jbalogh@mozilla.com', 14 | url='http://github.com/mozilla/django-session-csrf', 15 | license='BSD', 16 | packages=find_packages(), 17 | include_package_data=True, 18 | zip_safe=False, 19 | classifiers=[ 20 | 'Development Status :: 4 - Beta', 21 | 'Environment :: Web Environment', 22 | 'Environment :: Web Environment :: Mozilla', 23 | 'Framework :: Django', 24 | 'Intended Audience :: Developers', 25 | 'License :: OSI Approved :: BSD License', 26 | 'Programming Language :: Python :: 2.7', 27 | 'Programming Language :: Python :: 3', 28 | 'Operating System :: OS Independent', 29 | 'Programming Language :: Python', 30 | 'Topic :: Software Development :: Libraries :: Python Modules', 31 | ] 32 | ) 33 | --------------------------------------------------------------------------------