Skip to content

Commit 04a545b

Browse files
Jody McIntyreJeff Balogh
authored andcommitted
Add ANON_ALWAYS setting that enables CSRF protection for all anonymous views.
1 parent a8c4b64 commit 04a545b

File tree

3 files changed

+124
-7
lines changed

3 files changed

+124
-7
lines changed

README.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ anonymous view is protected through a CAPTCHA, for example.
9090
...
9191

9292

93+
If you want all views to have CSRF protection for anonymous users, use
94+
the following setting:
95+
96+
``ANON_ALWAYS``
97+
always provide CSRF protection for anonymous users
98+
99+
Default: False
100+
101+
93102
Why do I want this?
94103
-------------------
95104

session_csrf/__init__.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
ANON_COOKIE = getattr(settings, 'ANON_COOKIE', 'anoncsrf')
1212
ANON_TIMEOUT = getattr(settings, 'ANON_TIMEOUT', 60 * 60 * 2) # 2 hours.
13+
ANON_ALWAYS = getattr(settings, 'ANON_ALWAYS', False)
1314

1415

1516
# This overrides django.core.context_processors.csrf to dump our csrf_token
@@ -43,11 +44,20 @@ def process_request(self, request):
4344
request.csrf_token = request.session['csrf_token'] = token
4445
else:
4546
request.csrf_token = request.session['csrf_token']
46-
elif ANON_COOKIE in request.COOKIES:
47-
key = request.COOKIES[ANON_COOKIE]
48-
request.csrf_token = cache.get(key, '')
4947
else:
50-
request.csrf_token = ''
48+
key = None
49+
token = ''
50+
if ANON_COOKIE in request.COOKIES:
51+
key = request.COOKIES[ANON_COOKIE]
52+
token = cache.get(key, '')
53+
if ANON_ALWAYS:
54+
if not key:
55+
key = django_csrf._get_new_csrf_key()
56+
if not token:
57+
token = django_csrf._get_new_csrf_key()
58+
request._anon_csrf_key = key
59+
cache.set(key, token, ANON_TIMEOUT)
60+
request.csrf_token = token
5161

5262
def process_view(self, request, view_func, args, kwargs):
5363
"""Check the CSRF token if this is a POST."""
@@ -89,13 +99,22 @@ def process_view(self, request, view_func, args, kwargs):
8999
else:
90100
return self._accept(request)
91101

102+
def process_response(self, request, response):
103+
if hasattr(request, '_anon_csrf_key'):
104+
# Set or reset the cache and cookie timeouts.
105+
response.set_cookie(ANON_COOKIE, request._anon_csrf_key,
106+
max_age=ANON_TIMEOUT, httponly=True,
107+
secure=request.is_secure())
108+
patch_vary_headers(response, ['Cookie'])
109+
return response
110+
92111

93112
def anonymous_csrf(f):
94113
"""Decorator that assigns a CSRF token to an anonymous user."""
95114
@functools.wraps(f)
96115
def wrapper(request, *args, **kw):
97-
anon = not request.user.is_authenticated()
98-
if anon:
116+
use_anon_cookie = not (request.user.is_authenticated() or ANON_ALWAYS)
117+
if use_anon_cookie:
99118
if ANON_COOKIE in request.COOKIES:
100119
key = request.COOKIES[ANON_COOKIE]
101120
token = cache.get(key) or django_csrf._get_new_csrf_key()
@@ -105,7 +124,7 @@ def wrapper(request, *args, **kw):
105124
cache.set(key, token, ANON_TIMEOUT)
106125
request.csrf_token = token
107126
response = f(request, *args, **kw)
108-
if anon:
127+
if use_anon_cookie:
109128
# Set or reset the cache and cookie timeouts.
110129
response.set_cookie(ANON_COOKIE, key, max_age=ANON_TIMEOUT,
111130
httponly=True, secure=request.is_secure())

session_csrf/tests.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from contextlib import contextmanager
2+
13
import django.test
24
from django import http
35
from django.conf.urls.defaults import patterns
@@ -12,6 +14,7 @@
1214

1315
import mock
1416

17+
import session_csrf
1518
from session_csrf import CsrfMiddleware, anonymous_csrf, anonymous_csrf_exempt
1619

1720

@@ -28,6 +31,11 @@ class TestCsrfToken(django.test.TestCase):
2831
def setUp(self):
2932
self.client.handler = ClientHandler()
3033
User.objects.create_user('jbalogh', 'j@moz.com', 'password')
34+
self.save_ANON_ALWAYS = session_csrf.ANON_ALWAYS
35+
session_csrf.ANON_ALWAYS = False
36+
37+
def tearDown(self):
38+
session_csrf.ANON_ALWAYS = self.save_ANON_ALWAYS
3139

3240
def login(self):
3341
assert self.client.login(username='jbalogh', password='password')
@@ -155,6 +163,11 @@ def setUp(self):
155163
self.rf = django.test.RequestFactory()
156164
User.objects.create_user('jbalogh', 'j@moz.com', 'password')
157165
self.client.handler = ClientHandler(enforce_csrf_checks=True)
166+
self.save_ANON_ALWAYS = session_csrf.ANON_ALWAYS
167+
session_csrf.ANON_ALWAYS = False
168+
169+
def tearDown(self):
170+
session_csrf.ANON_ALWAYS = self.save_ANON_ALWAYS
158171

159172
def login(self):
160173
assert self.client.login(username='jbalogh', password='password')
@@ -238,6 +251,82 @@ def test_anonymous_csrf_exempt(self):
238251
self.assertEqual(response.status_code, 403)
239252

240253

254+
class TestAnonAlways(django.test.TestCase):
255+
# Repeats some tests with ANON_ALWAYS = True
256+
urls = 'session_csrf.tests'
257+
258+
def setUp(self):
259+
self.token = 'a' * 32
260+
self.rf = django.test.RequestFactory()
261+
User.objects.create_user('jbalogh', 'j@moz.com', 'password')
262+
self.client.handler = ClientHandler(enforce_csrf_checks=True)
263+
self.save_ANON_ALWAYS = session_csrf.ANON_ALWAYS
264+
session_csrf.ANON_ALWAYS = True
265+
266+
def tearDown(self):
267+
session_csrf.ANON_ALWAYS = self.save_ANON_ALWAYS
268+
269+
def login(self):
270+
assert self.client.login(username='jbalogh', password='password')
271+
272+
def test_csrftoken_unauthenticated(self):
273+
# request.csrf_token is set for anonymous users
274+
# when ANON_ALWAYS is enabled.
275+
response = self.client.get('/', follow=True)
276+
# The CSRF token is a 32-character MD5 string.
277+
self.assertEqual(len(response._request.csrf_token), 32)
278+
279+
def test_authenticated_request(self):
280+
# Nothing special happens, nothing breaks.
281+
# Find the CSRF token in the session.
282+
self.login()
283+
response = self.client.get('/', follow=True)
284+
sessionid = response.cookies['sessionid'].value
285+
session = Session.objects.get(session_key=sessionid)
286+
token = session.get_decoded()['csrf_token']
287+
288+
response = self.client.post('/', follow=True, HTTP_X_CSRFTOKEN=token)
289+
self.assertEqual(response.status_code, 200)
290+
291+
def test_unauthenticated_request(self):
292+
# We get a 403 since we're not sending a token.
293+
response = self.client.post('/')
294+
self.assertEqual(response.status_code, 403)
295+
296+
def test_new_anon_token_on_request(self):
297+
# A new anon user gets a key+token on the request and response.
298+
response = self.client.get('/')
299+
# Get the key from the cookie and find the token in the cache.
300+
key = response.cookies['anoncsrf'].value
301+
self.assertEqual(response._request.csrf_token, cache.get(key))
302+
303+
def test_existing_anon_cookie_on_request(self):
304+
# We reuse an existing anon cookie key+token.
305+
response = self.client.get('/')
306+
key = response.cookies['anoncsrf'].value
307+
308+
# Now check that subsequent requests use that cookie.
309+
response = self.client.get('/')
310+
self.assertEqual(response.cookies['anoncsrf'].value, key)
311+
self.assertEqual(response._request.csrf_token, cache.get(key))
312+
self.assertEqual(response['Vary'], 'Cookie')
313+
314+
def test_anon_csrf_logout(self):
315+
# Beware of views that logout the user.
316+
self.login()
317+
response = self.client.get('/logout')
318+
self.assertEqual(response.status_code, 200)
319+
320+
def test_existing_anon_cookie_not_in_cache(self):
321+
response = self.client.get('/')
322+
self.assertEqual(len(response._request.csrf_token), 32)
323+
324+
# Clear cache and make sure we still get a token
325+
cache.clear()
326+
response = self.client.get('/')
327+
self.assertEqual(len(response._request.csrf_token), 32)
328+
329+
241330
class ClientHandler(django.test.client.ClientHandler):
242331
"""
243332
Handler that stores the real request object on the response.

0 commit comments

Comments
 (0)