├── .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 |
--------------------------------------------------------------------------------