From 7230a995ce81a7b8dd093bd03cc5ebd34106ee80 Mon Sep 17 00:00:00 2001 From: Luke Plant Date: Tue, 27 Oct 2009 00:36:34 +0000 Subject: Moved contrib.csrf.* to core code. There is stub code for backwards compatiblity with Django 1.1 imports. The documentation has been updated, but has been left in docs/contrib/csrf.txt for now, in order to avoid dead links to documentation on the website. git-svn-id: http://code.djangoproject.com/svn/django/trunk@11661 bcc190cf-cafb-0310-a4f2-bffc1f526a37 --- django/conf/global_settings.py | 4 +- django/conf/project_template/settings.py | 2 +- django/contrib/admin/options.py | 2 +- django/contrib/admin/sites.py | 3 +- django/contrib/auth/views.py | 2 +- django/contrib/comments/views/comments.py | 2 +- django/contrib/comments/views/moderation.py | 2 +- django/contrib/csrf/context_processors.py | 20 -- django/contrib/csrf/decorators.py | 10 - django/contrib/csrf/middleware.py | 299 +----------------------- django/contrib/csrf/models.py | 1 - django/contrib/csrf/tests.py | 323 -------------------------- django/contrib/csrf/views.py | 62 ----- django/contrib/formtools/wizard.py | 2 +- django/core/context_processors.py | 19 ++ django/middleware/csrf.py | 262 ++++++++++++++++++++++ django/template/context.py | 2 +- django/views/csrf.py | 62 +++++ django/views/decorators/csrf.py | 47 ++++ docs/internals/deprecation.txt | 3 + docs/ref/contrib/csrf.txt | 44 +++- docs/ref/middleware.txt | 4 +- docs/ref/settings.txt | 4 +- docs/ref/templates/api.txt | 6 +- docs/releases/1.2-alpha.txt | 3 + docs/topics/http/middleware.txt | 2 +- tests/regressiontests/csrf_tests/__init__.py | 0 tests/regressiontests/csrf_tests/models.py | 1 + tests/regressiontests/csrf_tests/tests.py | 324 +++++++++++++++++++++++++++ 29 files changed, 778 insertions(+), 739 deletions(-) delete mode 100644 django/contrib/csrf/context_processors.py delete mode 100644 django/contrib/csrf/decorators.py delete mode 100644 django/contrib/csrf/models.py delete mode 100644 django/contrib/csrf/tests.py delete mode 100644 django/contrib/csrf/views.py create mode 100644 django/middleware/csrf.py create mode 100644 django/views/csrf.py create mode 100644 django/views/decorators/csrf.py create mode 100644 tests/regressiontests/csrf_tests/__init__.py create mode 100644 tests/regressiontests/csrf_tests/models.py create mode 100644 tests/regressiontests/csrf_tests/tests.py diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 62c7dd90c2..a0ce96a818 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -300,7 +300,7 @@ DEFAULT_INDEX_TABLESPACE = '' MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.csrf.middleware.CsrfViewMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', # 'django.middleware.http.ConditionalGetMiddleware', # 'django.middleware.gzip.GZipMiddleware', @@ -381,7 +381,7 @@ PASSWORD_RESET_TIMEOUT_DAYS = 3 # Dotted path to callable to be used as view when a request is # rejected by the CSRF middleware. -CSRF_FAILURE_VIEW = 'django.contrib.csrf.views.csrf_failure' +CSRF_FAILURE_VIEW = 'django.views.csrf.csrf_failure' # Name and domain for CSRF cookie. CSRF_COOKIE_NAME = 'csrftoken' diff --git a/django/conf/project_template/settings.py b/django/conf/project_template/settings.py index f83f3d505a..9b0b516c80 100644 --- a/django/conf/project_template/settings.py +++ b/django/conf/project_template/settings.py @@ -60,7 +60,7 @@ TEMPLATE_LOADERS = ( MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.csrf.middleware.CsrfViewMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', ) diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index c702e87340..0119e430b8 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -6,7 +6,7 @@ from django.contrib.contenttypes.models import ContentType from django.contrib.admin import widgets from django.contrib.admin import helpers from django.contrib.admin.util import unquote, flatten_fieldsets, get_deleted_objects, model_ngettext, model_format_dict -from django.contrib.csrf.decorators import csrf_protect +from django.views.decorators.csrf import csrf_protect from django.core.exceptions import PermissionDenied from django.db import models, transaction from django.db.models.fields import BLANK_CHOICE_DASH diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index d686540e56..33126999c8 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -3,8 +3,7 @@ from django import http, template from django.contrib.admin import ModelAdmin from django.contrib.admin import actions from django.contrib.auth import authenticate, login -from django.contrib.csrf.middleware import csrf_response_exempt -from django.contrib.csrf.decorators import csrf_protect +from django.views.decorators.csrf import csrf_protect, csrf_response_exempt from django.db.models.base import ModelBase from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import reverse diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py index 9d36710211..d427874df0 100644 --- a/django/contrib/auth/views.py +++ b/django/contrib/auth/views.py @@ -4,7 +4,7 @@ from django.contrib.auth.decorators import login_required from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm, PasswordChangeForm from django.contrib.auth.tokens import default_token_generator -from django.contrib.csrf.decorators import csrf_protect +from django.views.decorators.csrf import csrf_protect from django.core.urlresolvers import reverse from django.shortcuts import render_to_response, get_object_or_404 from django.contrib.sites.models import Site, RequestSite diff --git a/django/contrib/comments/views/comments.py b/django/contrib/comments/views/comments.py index ada7e9c77e..7fbe80eead 100644 --- a/django/contrib/comments/views/comments.py +++ b/django/contrib/comments/views/comments.py @@ -10,7 +10,7 @@ from django.utils.html import escape from django.views.decorators.http import require_POST from django.contrib import comments from django.contrib.comments import signals -from django.contrib.csrf.decorators import csrf_protect +from django.views.decorators.csrf import csrf_protect class CommentPostBadRequest(http.HttpResponseBadRequest): """ diff --git a/django/contrib/comments/views/moderation.py b/django/contrib/comments/views/moderation.py index 76db326c31..73304ba416 100644 --- a/django/contrib/comments/views/moderation.py +++ b/django/contrib/comments/views/moderation.py @@ -5,7 +5,7 @@ from django.contrib.auth.decorators import login_required, permission_required from utils import next_redirect, confirmation_view from django.contrib import comments from django.contrib.comments import signals -from django.contrib.csrf.decorators import csrf_protect +from django.views.decorators.csrf import csrf_protect @csrf_protect @login_required diff --git a/django/contrib/csrf/context_processors.py b/django/contrib/csrf/context_processors.py deleted file mode 100644 index b78030a0b2..0000000000 --- a/django/contrib/csrf/context_processors.py +++ /dev/null @@ -1,20 +0,0 @@ -from django.contrib.csrf.middleware import get_token -from django.utils.functional import lazy - -def csrf(request): - """ - Context processor that provides a CSRF token, or the string 'NOTPROVIDED' if - it has not been provided by either a view decorator or the middleware - """ - def _get_val(): - token = get_token(request) - if token is None: - # In order to be able to provide debugging info in the - # case of misconfiguration, we use a sentinel value - # instead of returning an empty dict. - return 'NOTPROVIDED' - else: - return token - _get_val = lazy(_get_val, str) - - return {'csrf_token': _get_val() } diff --git a/django/contrib/csrf/decorators.py b/django/contrib/csrf/decorators.py deleted file mode 100644 index 67e33bce5c..0000000000 --- a/django/contrib/csrf/decorators.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.contrib.csrf.middleware import CsrfViewMiddleware -from django.utils.decorators import decorator_from_middleware - -csrf_protect = decorator_from_middleware(CsrfViewMiddleware) -csrf_protect.__name__ = "csrf_protect" -csrf_protect.__doc__ = """ -This decorator adds CSRF protection in exactly the same way as -CsrfViewMiddleware, but it can be used on a per view basis. Using both, or -using the decorator multiple times, is harmless and efficient. -""" diff --git a/django/contrib/csrf/middleware.py b/django/contrib/csrf/middleware.py index daee12379e..4885cfcc3e 100644 --- a/django/contrib/csrf/middleware.py +++ b/django/contrib/csrf/middleware.py @@ -1,294 +1,7 @@ -""" -Cross Site Request Forgery Middleware. +from django.middleware.csrf import CsrfMiddleware, CsrfViewMiddleware, CsrfResponseMiddleware +from django.views.decorators.csrf import csrf_exempt, csrf_view_exempt, csrf_response_exempt -This module provides a middleware that implements protection -against request forgeries from other sites. -""" - -import itertools -import re -import random -try: - from functools import wraps -except ImportError: - from django.utils.functional import wraps # Python 2.3, 2.4 fallback. - -from django.conf import settings -from django.core.urlresolvers import get_callable -from django.utils.cache import patch_vary_headers -from django.utils.hashcompat import md5_constructor -from django.utils.safestring import mark_safe - -_POST_FORM_RE = \ - re.compile(r'(]*\bmethod\s*=\s*(\'|"|)POST(\'|"|)\b[^>]*>)', re.IGNORECASE) - -_HTML_TYPES = ('text/html', 'application/xhtml+xml') - -# Use the system (hardware-based) random number generator if it exists. -if hasattr(random, 'SystemRandom'): - randrange = random.SystemRandom().randrange -else: - randrange = random.randrange -_MAX_CSRF_KEY = 18446744073709551616L # 2 << 63 - -def _get_failure_view(): - """ - Returns the view to be used for CSRF rejections - """ - return get_callable(settings.CSRF_FAILURE_VIEW) - -def _get_new_csrf_key(): - return md5_constructor("%s%s" - % (randrange(0, _MAX_CSRF_KEY), settings.SECRET_KEY)).hexdigest() - -def _make_legacy_session_token(session_id): - return md5_constructor(settings.SECRET_KEY + session_id).hexdigest() - -def get_token(request): - """ - Returns the the CSRF token required for a POST form. - - A side effect of calling this function is to make the the csrf_protect - decorator and the CsrfViewMiddleware add a CSRF cookie and a 'Vary: Cookie' - header to the outgoing response. For this reason, you may need to use this - function lazily, as is done by the csrf context processor. - """ - request.META["CSRF_COOKIE_USED"] = True - return request.META.get("CSRF_COOKIE", None) - -class CsrfViewMiddleware(object): - """ - Middleware that requires a present and correct csrfmiddlewaretoken - for POST requests that have a CSRF cookie, and sets an outgoing - CSRF cookie. - - This middleware should be used in conjunction with the csrf_token template - tag. - """ - def process_view(self, request, callback, callback_args, callback_kwargs): - if getattr(callback, 'csrf_exempt', False): - return None - - if getattr(request, 'csrf_processing_done', False): - return None - - reject = lambda s: _get_failure_view()(request, reason=s) - def accept(): - # Avoid checking the request twice by adding a custom attribute to - # request. This will be relevant when both decorator and middleware - # are used. - request.csrf_processing_done = True - return None - - # If the user doesn't have a CSRF cookie, generate one and store it in the - # request, so it's available to the view. We'll store it in a cookie when - # we reach the response. - try: - request.META["CSRF_COOKIE"] = request.COOKIES[settings.CSRF_COOKIE_NAME] - cookie_is_new = False - except KeyError: - # No cookie, so create one. - request.META["CSRF_COOKIE"] = _get_new_csrf_key() - cookie_is_new = True - - if request.method == 'POST': - if getattr(request, '_dont_enforce_csrf_checks', False): - # Mechanism to turn off CSRF checks for test suite. It comes after - # the creation of CSRF cookies, so that everything else continues to - # work exactly the same (e.g. cookies are sent etc), but before the - # any branches that call reject() - return accept() - - if request.is_ajax(): - # .is_ajax() is based on the presence of X-Requested-With. In - # the context of a browser, this can only be sent if using - # XmlHttpRequest. Browsers implement careful policies for - # XmlHttpRequest: - # - # * Normally, only same-domain requests are allowed. - # - # * Some browsers (e.g. Firefox 3.5 and later) relax this - # carefully: - # - # * if it is a 'simple' GET or POST request (which can - # include no custom headers), it is allowed to be cross - # domain. These requests will not be recognized as AJAX. - # - # * if a 'preflight' check with the server confirms that the - # server is expecting and allows the request, cross domain - # requests even with custom headers are allowed. These - # requests will be recognized as AJAX, but can only get - # through when the developer has specifically opted in to - # allowing the cross-domain POST request. - # - # So in all cases, it is safe to allow these requests through. - return accept() - - if request.is_secure(): - # Strict referer checking for HTTPS - referer = request.META.get('HTTP_REFERER') - if referer is None: - return reject("Referer checking failed - no Referer.") - - # The following check ensures that the referer is HTTPS, - # the domains match and the ports match. This might be too strict. - good_referer = 'https://%s/' % request.get_host() - if not referer.startswith(good_referer): - return reject("Referer checking failed - %s does not match %s." % - (referer, good_referer)) - - # If the user didn't already have a CSRF key, then accept the - # session key for the middleware token, so CSRF protection isn't lost - # for the period between upgrading to CSRF cookes to the first time - # each user comes back to the site to receive one. - if cookie_is_new: - try: - session_id = request.COOKIES[settings.SESSION_COOKIE_NAME] - csrf_token = _make_legacy_session_token(session_id) - except KeyError: - # No CSRF cookie and no session cookie. For POST requests, - # we insist on a CSRF cookie, and in this way we can avoid - # all CSRF attacks, including login CSRF. - return reject("No CSRF cookie.") - else: - csrf_token = request.META["CSRF_COOKIE"] - - # check incoming token - request_csrf_token = request.POST.get('csrfmiddlewaretoken', None) - if request_csrf_token != csrf_token: - return reject("CSRF token missing or incorrect.") - - return accept() - - def process_response(self, request, response): - if getattr(response, 'csrf_processing_done', False): - return response - - # If CSRF_COOKIE is unset, then CsrfViewMiddleware.process_view was - # never called, probaby because a request middleware returned a response - # (for example, contrib.auth redirecting to a login page). - if request.META.get("CSRF_COOKIE") is None: - return response - - if not request.META.get("CSRF_COOKIE_USED", False): - return response - - # Set the CSRF cookie even if it's already set, so we renew the expiry timer. - response.set_cookie(settings.CSRF_COOKIE_NAME, - request.META["CSRF_COOKIE"], max_age = 60 * 60 * 24 * 7 * 52, - domain=settings.CSRF_COOKIE_DOMAIN) - # Content varies with the CSRF cookie, so set the Vary header. - patch_vary_headers(response, ('Cookie',)) - response.csrf_processing_done = True - return response - -class CsrfResponseMiddleware(object): - """ - DEPRECATED - Middleware that post-processes a response to add a csrfmiddlewaretoken. - - This exists for backwards compatibility and as an interim measure until - applications are converted to using use the csrf_token template tag - instead. It will be removed in Django 1.4. - """ - def __init__(self): - import warnings - warnings.warn( - "CsrfResponseMiddleware and CsrfMiddleware are deprecated; use CsrfViewMiddleware and the template tag instead (see CSRF documentation).", - PendingDeprecationWarning - ) - - def process_response(self, request, response): - if getattr(response, 'csrf_exempt', False): - return response - - if response['Content-Type'].split(';')[0] in _HTML_TYPES: - csrf_token = get_token(request) - # If csrf_token is None, we have no token for this request, which probably - # means that this is a response from a request middleware. - if csrf_token is None: - return response - - # ensure we don't add the 'id' attribute twice (HTML validity) - idattributes = itertools.chain(("id='csrfmiddlewaretoken'",), - itertools.repeat('')) - def add_csrf_field(match): - """Returns the matched
tag plus the added element""" - return mark_safe(match.group() + "
" + \ - "
") - - # Modify any POST forms - response.content, n = _POST_FORM_RE.subn(add_csrf_field, response.content) - if n > 0: - # Content varies with the CSRF cookie, so set the Vary header. - patch_vary_headers(response, ('Cookie',)) - - # Since the content has been modified, any Etag will now be - # incorrect. We could recalculate, but only if we assume that - # the Etag was set by CommonMiddleware. The safest thing is just - # to delete. See bug #9163 - del response['ETag'] - return response - -class CsrfMiddleware(object): - """ - Django middleware that adds protection against Cross Site - Request Forgeries by adding hidden form fields to POST forms and - checking requests for the correct value. - - CsrfMiddleware uses two middleware, CsrfViewMiddleware and - CsrfResponseMiddleware, which can be used independently. It is recommended - to use only CsrfViewMiddleware and use the csrf_token template tag in - templates for inserting the token. - """ - # We can't just inherit from CsrfViewMiddleware and CsrfResponseMiddleware - # because both have process_response methods. - def __init__(self): - self.response_middleware = CsrfResponseMiddleware() - self.view_middleware = CsrfViewMiddleware() - - def process_response(self, request, resp): - # We must do the response post-processing first, because that calls - # get_token(), which triggers a flag saying that the CSRF cookie needs - # to be sent (done in CsrfViewMiddleware.process_response) - resp2 = self.response_middleware.process_response(request, resp) - return self.view_middleware.process_response(request, resp2) - - def process_view(self, request, callback, callback_args, callback_kwargs): - return self.view_middleware.process_view(request, callback, callback_args, - callback_kwargs) - -def csrf_response_exempt(view_func): - """ - Modifies a view function so that its response is exempt - from the post-processing of the CSRF middleware. - """ - def wrapped_view(*args, **kwargs): - resp = view_func(*args, **kwargs) - resp.csrf_exempt = True - return resp - return wraps(view_func)(wrapped_view) - -def csrf_view_exempt(view_func): - """ - Marks a view function as being exempt from CSRF view protection. - """ - # We could just do view_func.csrf_exempt = True, but decorators - # are nicer if they don't have side-effects, so we return a new - # function. - def wrapped_view(*args, **kwargs): - return view_func(*args, **kwargs) - wrapped_view.csrf_exempt = True - return wraps(view_func)(wrapped_view) - -def csrf_exempt(view_func): - """ - Marks a view function as being exempt from the CSRF checks - and post processing. - - This is the same as using both the csrf_view_exempt and - csrf_response_exempt decorators. - """ - return csrf_response_exempt(csrf_view_exempt(view_func)) +import warnings +warnings.warn("This import for CSRF functionality is deprecated. Please use django.middleware.csrf for the middleware and django.views.decorators.csrf for decorators.", + PendingDeprecationWarning + ) diff --git a/django/contrib/csrf/models.py b/django/contrib/csrf/models.py deleted file mode 100644 index 71abcc5198..0000000000 --- a/django/contrib/csrf/models.py +++ /dev/null @@ -1 +0,0 @@ -# models.py file for tests to run. diff --git a/django/contrib/csrf/tests.py b/django/contrib/csrf/tests.py deleted file mode 100644 index 14015736c4..0000000000 --- a/django/contrib/csrf/tests.py +++ /dev/null @@ -1,323 +0,0 @@ -# -*- coding: utf-8 -*- - -from django.test import TestCase -from django.http import HttpRequest, HttpResponse -from django.contrib.csrf.middleware import CsrfMiddleware, CsrfViewMiddleware, csrf_exempt -from django.contrib.csrf.context_processors import csrf -from django.contrib.sessions.middleware import SessionMiddleware -from django.utils.importlib import import_module -from django.conf import settings -from django.template import RequestContext, Template - -# Response/views used for CsrfResponseMiddleware and CsrfViewMiddleware tests -def post_form_response(): - resp = HttpResponse(content=""" -
-""", mimetype="text/html") - return resp - -def post_form_response_non_html(): - resp = post_form_response() - resp["Content-Type"] = "application/xml" - return resp - -def post_form_view(request): - """A view that returns a POST form (without a token)""" - return post_form_response() - -# Response/views used for template tag tests -def _token_template(): - return Template("{% csrf_token %}") - -def _render_csrf_token_template(req): - context = RequestContext(req, processors=[csrf]) - template = _token_template() - return template.render(context) - -def token_view(request): - """A view that uses {% csrf_token %}""" - return HttpResponse(_render_csrf_token_template(request)) - -def non_token_view_using_request_processor(request): - """ - A view that doesn't use the token, but does use the csrf view processor. - """ - context = RequestContext(request, processors=[csrf]) - template = Template("") - return HttpResponse(template.render(context)) - -class TestingHttpRequest(HttpRequest): - """ - A version of HttpRequest that allows us to change some things - more easily - """ - def is_secure(self): - return getattr(self, '_is_secure', False) - -class CsrfMiddlewareTest(TestCase): - _csrf_id = "1" - - # This is a valid session token for this ID and secret key. This was generated using - # the old code that we're to be backwards-compatible with. Don't use the CSRF code - # to generate this hash, or we're merely testing the code against itself and not - # checking backwards-compatibility. This is also the output of (echo -n test1 | md5sum). - _session_token = "5a105e8b9d40e1329780d62ea2265d8a" - _session_id = "1" - _secret_key_for_session_test= "test" - - def _get_GET_no_csrf_cookie_request(self): - return TestingHttpRequest() - - def _get_GET_csrf_cookie_request(self): - req = TestingHttpRequest() - req.COOKIES[settings.CSRF_COOKIE_NAME] = self._csrf_id - return req - - def _get_POST_csrf_cookie_request(self): - req = self._get_GET_csrf_cookie_request() - req.method = "POST" - return req - - def _get_POST_no_csrf_cookie_request(self): - req = self._get_GET_no_csrf_cookie_request() - req.method = "POST" - return req - - def _get_POST_request_with_token(self): - req = self._get_POST_csrf_cookie_request() - req.POST['csrfmiddlewaretoken'] = self._csrf_id - return req - - def _get_POST_session_request_with_token(self): - req = self._get_POST_no_csrf_cookie_request() - req.COOKIES[settings.SESSION_COOKIE_NAME] = self._session_id - req.POST['csrfmiddlewaretoken'] = self._session_token - return req - - def _get_POST_session_request_no_token(self): - req = self._get_POST_no_csrf_cookie_request() - req.COOKIES[settings.SESSION_COOKIE_NAME] = self._session_id - return req - - def _check_token_present(self, response, csrf_id=None): - self.assertContains(response, "name='csrfmiddlewaretoken' value='%s'" % (csrf_id or self._csrf_id)) - - # Check the post processing and outgoing cookie - def test_process_response_no_csrf_cookie(self): - """ - When no prior CSRF cookie exists, check that the cookie is created and a - token is inserted. - """ - req = self._get_GET_no_csrf_cookie_request() - CsrfMiddleware().process_view(req, post_form_view, (), {}) - - resp = post_form_response() - resp_content = resp.content # needed because process_response modifies resp - resp2 = CsrfMiddleware().process_response(req, resp) - - csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False) - self.assertNotEqual(csrf_cookie, False) - self.assertNotEqual(resp_content, resp2.content) - self._check_token_present(resp2, csrf_cookie.value) - # Check the Vary header got patched correctly - self.assert_('Cookie' in resp2.get('Vary','')) - - def test_process_response_no_csrf_cookie_view_only_get_token_used(self): - """ - When no prior CSRF cookie exists, check that the cookie is created, even - if only CsrfViewMiddleware is used. - """ - # This is checking that CsrfViewMiddleware has the cookie setting - # code. Most of the other tests use CsrfMiddleware. - req = self._get_GET_no_csrf_cookie_request() - # token_view calls get_token() indirectly - CsrfViewMiddleware().process_view(req, token_view, (), {}) - resp = token_view(req) - resp2 = CsrfViewMiddleware().process_response(req, resp) - - csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False) - self.assertNotEqual(csrf_cookie, False) - - def test_process_response_get_token_not_used(self): - """ - Check that if get_token() is not called, the view middleware does not - add a cookie. - """ - # This is important to make pages cacheable. Pages which do call - # get_token(), assuming they use the token, are not cacheable because - # the token is specific to the user - req = self._get_GET_no_csrf_cookie_request() - # non_token_view_using_request_processor does not call get_token(), but - # does use the csrf request processor. By using this, we are testing - # that the view processor is properly lazy and doesn't call get_token() - # until needed. - CsrfViewMiddleware().process_view(req, non_token_view_using_request_processor, (), {}) - resp = non_token_view_using_request_processor(req) - resp2 = CsrfViewMiddleware().process_response(req, resp) - - csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False) - self.assertEqual(csrf_cookie, False) - - def test_process_response_existing_csrf_cookie(self): - """ - Check that the token is inserted when a prior CSRF cookie exists - """ - req = self._get_GET_csrf_cookie_request() - CsrfMiddleware().process_view(req, post_form_view, (), {}) - - resp = post_form_response() - resp_content = resp.content # needed because process_response modifies resp - resp2 = CsrfMiddleware().process_response(req, resp) - self.assertNotEqual(resp_content, resp2.content) - self._check_token_present(resp2) - - def test_process_response_non_html(self): - """ - Check the the post-processor does nothing for content-types not in _HTML_TYPES. - """ - req = self._get_GET_no_csrf_cookie_request() - CsrfMiddleware().process_view(req, post_form_view, (), {}) - resp = post_form_response_non_html() - resp_content = resp.content # needed because process_response modifies resp - resp2 = CsrfMiddleware().process_response(req, resp) - self.assertEquals(resp_content, resp2.content) - - def test_process_response_exempt_view(self): - """ - Check that no post processing is done for an exempt view - """ - req = self._get_POST_csrf_cookie_request() - resp = csrf_exempt(post_form_view)(req) - resp_content = resp.content - resp2 = CsrfMiddleware().process_response(req, resp) - self.assertEquals(resp_content, resp2.content) - - # Check the request processing - def test_process_request_no_session_no_csrf_cookie(self): - """ - Check that if neither a CSRF cookie nor a session cookie are present, - the middleware rejects the incoming request. This will stop login CSRF. - """ - req = self._get_POST_no_csrf_cookie_request() - req2 = CsrfMiddleware().process_view(req, post_form_view, (), {}) - self.assertEquals(403, req2.status_code) - - def test_process_request_csrf_cookie_no_token(self): - """ - Check that if a CSRF cookie is present but no token, the middleware - rejects the incoming request. - """ - req = self._get_POST_csrf_cookie_request() - req2 = CsrfMiddleware().process_view(req, post_form_view, (), {}) - self.assertEquals(403, req2.status_code) - - def test_process_request_csrf_cookie_and_token(self): - """ - Check that if both a cookie and a token is present, the middleware lets it through. - """ - req = self._get_POST_request_with_token() - req2 = CsrfMiddleware().process_view(req, post_form_view, (), {}) - self.assertEquals(None, req2) - - def test_process_request_session_cookie_no_csrf_cookie_token(self): - """ - When no CSRF cookie exists, but the user has a session, check that a token - using the session cookie as a legacy CSRF cookie is accepted. - """ - orig_secret_key = settings.SECRET_KEY - settings.SECRET_KEY = self._secret_key_for_session_test - try: - req = self._get_POST_session_request_with_token() - req2 = CsrfMiddleware().process_view(req, post_form_view, (), {}) - self.assertEquals(None, req2) - finally: - settings.SECRET_KEY = orig_secret_key - - def test_process_request_session_cookie_no_csrf_cookie_no_token(self): - """ - Check that if a session cookie is present but no token and no CSRF cookie, - the request is rejected. - """ - req = self._get_POST_session_request_no_token() - req2 = CsrfMiddleware().process_view(req, post_form_view, (), {}) - self.assertEquals(403, req2.status_code) - - def test_process_request_csrf_cookie_no_token_exempt_view(self): - """ - Check that if a CSRF cookie is present and no token, but the csrf_exempt - decorator has been applied to the view, the middleware lets it through - """ - req = self._get_POST_csrf_cookie_request() - req2 = CsrfMiddleware().process_view(req, csrf_exempt(post_form_view), (), {}) - self.assertEquals(None, req2) - - def test_ajax_exemption(self): - """ - Check that AJAX requests are automatically exempted. - """ - req = self._get_POST_csrf_cookie_request() - req.META['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' - req2 = CsrfMiddleware().process_view(req, post_form_view, (), {}) - self.assertEquals(None, req2) - - # Tests for the template tag method - def test_token_node_no_csrf_cookie(self): - """ - Check that CsrfTokenNode works when no CSRF cookie is set - """ - req = self._get_GET_no_csrf_cookie_request() - resp = token_view(req) - self.assertEquals(u"", resp.content) - - def test_token_node_with_csrf_cookie(self): - """ - Check that CsrfTokenNode works when a CSRF cookie is set - """ - req = self._get_GET_csrf_cookie_request() - CsrfViewMiddleware().process_view(req, token_view, (), {}) - resp = token_view(req) - self._check_token_present(resp) - - def test_token_node_with_new_csrf_cookie(self): - """ - Check that CsrfTokenNode works when a CSRF cookie is created by - the middleware (when one was not already present) - """ - req = self._get_GET_no_csrf_cookie_request() - CsrfViewMiddleware().process_view(req, token_view, (), {}) - resp = token_view(req) - resp2 = CsrfViewMiddleware().process_response(req, resp) - csrf_cookie = resp2.cookies[settings.CSRF_COOKIE_NAME] - self._check_token_present(resp, csrf_id=csrf_cookie.value) - - def test_response_middleware_without_view_middleware(self): - """ - Check that CsrfResponseMiddleware finishes without error if the view middleware - has not been called, as is the case if a request middleware returns a response. - """ - req = self._get_GET_no_csrf_cookie_request() - resp = post_form_view(req) - CsrfMiddleware().process_response(req, resp) - - def test_https_bad_referer(self): - """ - Test that a POST HTTPS request with a bad referer is rejected - """ - req = self._get_POST_request_with_token() - req._is_secure = True - req.META['HTTP_HOST'] = 'www.example.com' - req.META['HTTP_REFERER'] = 'https://www.evil.org/somepage' - req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {}) - self.assertNotEqual(None, req2) - self.assertEquals(403, req2.status_code) - - def test_https_good_referer(self): - """ - Test that a POST HTTPS request with a good referer is accepted - """ - req = self._get_POST_request_with_token() - req._is_secure = True - req.META['HTTP_HOST'] = 'www.example.com' - req.META['HTTP_REFERER'] = 'https://www.example.com/somepage' - req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {}) - self.assertEquals(None, req2) diff --git a/django/contrib/csrf/views.py b/django/contrib/csrf/views.py deleted file mode 100644 index dd8a8966b1..0000000000 --- a/django/contrib/csrf/views.py +++ /dev/null @@ -1,62 +0,0 @@ -from django.http import HttpResponseForbidden -from django.template import Context, Template -from django.conf import settings - -# We include the template inline since we need to be able to reliably display -# this error message, especially for the sake of developers, and there isn't any -# other way of making it available independent of what is in the settings file. - -CSRF_FAILRE_TEMPLATE = """ - - - - - 403 Forbidden - - -

403 Forbidden

-

CSRF verification failed. Request aborted.

- {% if DEBUG %} -

Help

- {% if reason %} -

Reason given for failure:

-
-    {{ reason }}
-    
- {% endif %} - -

In general, this can occur when there is a genuine Cross Site Request Forgery, or when - Django's - CSRF mechanism has not been used correctly. For POST forms, you need to - ensure:

- - - -

You're seeing the help section of this page because you have DEBUG = - True in your Django settings file. Change that to False, - and only the initial error message will be displayed.

- -

You can customize this page using the CSRF_FAILURE_VIEW setting.

- - {% endif %} - - -""" - -def csrf_failure(request, reason=""): - """ - Default view used when request fails CSRF protection - """ - t = Template(CSRF_FAILRE_TEMPLATE) - c = Context({'DEBUG': settings.DEBUG, - 'reason': reason}) - return HttpResponseForbidden(t.render(c), mimetype='text/html') diff --git a/django/contrib/formtools/wizard.py b/django/contrib/formtools/wizard.py index 60fe314217..4729e155b8 100644 --- a/django/contrib/formtools/wizard.py +++ b/django/contrib/formtools/wizard.py @@ -14,7 +14,7 @@ from django.template.context import RequestContext from django.utils.hashcompat import md5_constructor from django.utils.translation import ugettext_lazy as _ from django.contrib.formtools.utils import security_hash -from django.contrib.csrf.decorators import csrf_protect +from django.views.decorators.csrf import csrf_protect class FormWizard(object): # Dictionary of extra template context variables. diff --git a/django/core/context_processors.py b/django/core/context_processors.py index 0dbd8449b6..b950dba0f6 100644 --- a/django/core/context_processors.py +++ b/django/core/context_processors.py @@ -8,6 +8,7 @@ RequestContext. """ from django.conf import settings +from django.middleware.csrf import get_token from django.utils.functional import lazy, memoize, SimpleLazyObject def auth(request): @@ -40,6 +41,24 @@ def auth(request): 'perms': lazy(lambda: PermWrapper(get_user()), PermWrapper)(), } +def csrf(request): + """ + Context processor that provides a CSRF token, or the string 'NOTPROVIDED' if + it has not been provided by either a view decorator or the middleware + """ + def _get_val(): + token = get_token(request) + if token is None: + # In order to be able to provide debugging info in the + # case of misconfiguration, we use a sentinel value + # instead of returning an empty dict. + return 'NOTPROVIDED' + else: + return token + _get_val = lazy(_get_val, str) + + return {'csrf_token': _get_val() } + def debug(request): "Returns context variables helpful for debugging." context_extras = {} diff --git a/django/middleware/csrf.py b/django/middleware/csrf.py new file mode 100644 index 0000000000..339487bbcc --- /dev/null +++ b/django/middleware/csrf.py @@ -0,0 +1,262 @@ +""" +Cross Site Request Forgery Middleware. + +This module provides a middleware that implements protection +against request forgeries from other sites. +""" + +import itertools +import re +import random +try: + from functools import wraps +except ImportError: + from django.utils.functional import wraps # Python 2.3, 2.4 fallback. + +from django.conf import settings +from django.core.urlresolvers import get_callable +from django.utils.cache import patch_vary_headers +from django.utils.hashcompat import md5_constructor +from django.utils.safestring import mark_safe + +_POST_FORM_RE = \ + re.compile(r'(]*\bmethod\s*=\s*(\'|"|)POST(\'|"|)\b[^>]*>)', re.IGNORECASE) + +_HTML_TYPES = ('text/html', 'application/xhtml+xml') + +# Use the system (hardware-based) random number generator if it exists. +if hasattr(random, 'SystemRandom'): + randrange = random.SystemRandom().randrange +else: + randrange = random.randrange +_MAX_CSRF_KEY = 18446744073709551616L # 2 << 63 + +def _get_failure_view(): + """ + Returns the view to be used for CSRF rejections + """ + return get_callable(settings.CSRF_FAILURE_VIEW) + +def _get_new_csrf_key(): + return md5_constructor("%s%s" + % (randrange(0, _MAX_CSRF_KEY), settings.SECRET_KEY)).hexdigest() + +def _make_legacy_session_token(session_id): + return md5_constructor(settings.SECRET_KEY + session_id).hexdigest() + +def get_token(request): + """ + Returns the the CSRF token required for a POST form. + + A side effect of calling this function is to make the the csrf_protect + decorator and the CsrfViewMiddleware add a CSRF cookie and a 'Vary: Cookie' + header to the outgoing response. For this reason, you may need to use this + function lazily, as is done by the csrf context processor. + """ + request.META["CSRF_COOKIE_USED"] = True + return request.META.get("CSRF_COOKIE", None) + +class CsrfViewMiddleware(object): + """ + Middleware that requires a present and correct csrfmiddlewaretoken + for POST requests that have a CSRF cookie, and sets an outgoing + CSRF cookie. + + This middleware should be used in conjunction with the csrf_token template + tag. + """ + def process_view(self, request, callback, callback_args, callback_kwargs): + if getattr(callback, 'csrf_exempt', False): + return None + + if getattr(request, 'csrf_processing_done', False): + return None + + reject = lambda s: _get_failure_view()(request, reason=s) + def accept(): + # Avoid checking the request twice by adding a custom attribute to + # request. This will be relevant when both decorator and middleware + # are used. + request.csrf_processing_done = True + return None + + # If the user doesn't have a CSRF cookie, generate one and store it in the + # request, so it's available to the view. We'll store it in a cookie when + # we reach the response. + try: + request.META["CSRF_COOKIE"] = request.COOKIES[settings.CSRF_COOKIE_NAME] + cookie_is_new = False + except KeyError: + # No cookie, so create one. + request.META["CSRF_COOKIE"] = _get_new_csrf_key() + cookie_is_new = True + + if request.method == 'POST': + if getattr(request, '_dont_enforce_csrf_checks', False): + # Mechanism to turn off CSRF checks for test suite. It comes after + # the creation of CSRF cookies, so that everything else continues to + # work exactly the same (e.g. cookies are sent etc), but before the + # any branches that call reject() + return accept() + + if request.is_ajax(): + # .is_ajax() is based on the presence of X-Requested-With. In + # the context of a browser, this can only be sent if using + # XmlHttpRequest. Browsers implement careful policies for + # XmlHttpRequest: + # + # * Normally, only same-domain requests are allowed. + # + # * Some browsers (e.g. Firefox 3.5 and later) relax this + # carefully: + # + # * if it is a 'simple' GET or POST request (which can + # include no custom headers), it is allowed to be cross + # domain. These requests will not be recognized as AJAX. + # + # * if a 'preflight' check with the server confirms that the + # server is expecting and allows the request, cross domain + # requests even with custom headers are allowed. These + # requests will be recognized as AJAX, but can only get + # through when the developer has specifically opted in to + # allowing the cross-domain POST request. + # + # So in all cases, it is safe to allow these requests through. + return accept() + + if request.is_secure(): + # Strict referer checking for HTTPS + referer = request.META.get('HTTP_REFERER') + if referer is None: + return reject("Referer checking failed - no Referer.") + + # The following check ensures that the referer is HTTPS, + # the domains match and the ports match. This might be too strict. + good_referer = 'https://%s/' % request.get_host() + if not referer.startswith(good_referer): + return reject("Referer checking failed - %s does not match %s." % + (referer, good_referer)) + + # If the user didn't already have a CSRF key, then accept the + # session key for the middleware token, so CSRF protection isn't lost + # for the period between upgrading to CSRF cookes to the first time + # each user comes back to the site to receive one. + if cookie_is_new: + try: + session_id = request.COOKIES[settings.SESSION_COOKIE_NAME] + csrf_token = _make_legacy_session_token(session_id) + except KeyError: + # No CSRF cookie and no session cookie. For POST requests, + # we insist on a CSRF cookie, and in this way we can avoid + # all CSRF attacks, including login CSRF. + return reject("No CSRF cookie.") + else: + csrf_token = request.META["CSRF_COOKIE"] + + # check incoming token + request_csrf_token = request.POST.get('csrfmiddlewaretoken', None) + if request_csrf_token != csrf_token: + return reject("CSRF token missing or incorrect.") + + return accept() + + def process_response(self, request, response): + if getattr(response, 'csrf_processing_done', False): + return response + + # If CSRF_COOKIE is unset, then CsrfViewMiddleware.process_view was + # never called, probaby because a request middleware returned a response + # (for example, contrib.auth redirecting to a login page). + if request.META.get("CSRF_COOKIE") is None: + return response + + if not request.META.get("CSRF_COOKIE_USED", False): + return response + + # Set the CSRF cookie even if it's already set, so we renew the expiry timer. + response.set_cookie(settings.CSRF_COOKIE_NAME, + request.META["CSRF_COOKIE"], max_age = 60 * 60 * 24 * 7 * 52, + domain=settings.CSRF_COOKIE_DOMAIN) + # Content varies with the CSRF cookie, so set the Vary header. + patch_vary_headers(response, ('Cookie',)) + response.csrf_processing_done = True + return response + +class CsrfResponseMiddleware(object): + """ + DEPRECATED + Middleware that post-processes a response to add a csrfmiddlewaretoken. + + This exists for backwards compatibility and as an interim measure until + applications are converted to using use the csrf_token template tag + instead. It will be removed in Django 1.4. + """ + def __init__(self): + import warnings + warnings.warn( + "CsrfResponseMiddleware and CsrfMiddleware are deprecated; use CsrfViewMiddleware and the template tag instead (see CSRF documentation).", + PendingDeprecationWarning + ) + + def process_response(self, request, response): + if getattr(response, 'csrf_exempt', False): + return response + + if response['Content-Type'].split(';')[0] in _HTML_TYPES: + csrf_token = get_token(request) + # If csrf_token is None, we have no token for this request, which probably + # means that this is a response from a request middleware. + if csrf_token is None: + return response + + # ensure we don't add the 'id' attribute twice (HTML validity) + idattributes = itertools.chain(("id='csrfmiddlewaretoken'",), + itertools.repeat('')) + def add_csrf_field(match): + """Returns the matched
tag plus the added element""" + return mark_safe(match.group() + "
" + \ + "
") + + # Modify any POST forms + response.content, n = _POST_FORM_RE.subn(add_csrf_field, response.content) + if n > 0: + # Content varies with the CSRF cookie, so set the Vary header. + patch_vary_headers(response, ('Cookie',)) + + # Since the content has been modified, any Etag will now be + # incorrect. We could recalculate, but only if we assume that + # the Etag was set by CommonMiddleware. The safest thing is just + # to delete. See bug #9163 + del response['ETag'] + return response + +class CsrfMiddleware(object): + """ + Django middleware that adds protection against Cross Site + Request Forgeries by adding hidden form fields to POST forms and + checking requests for the correct value. + + CsrfMiddleware uses two middleware, CsrfViewMiddleware and + CsrfResponseMiddleware, which can be used independently. It is recommended + to use only CsrfViewMiddleware and use the csrf_token template tag in + templates for inserting the token. + """ + # We can't just inherit from CsrfViewMiddleware and CsrfResponseMiddleware + # because both have process_response methods. + def __init__(self): + self.response_middleware = CsrfResponseMiddleware() + self.view_middleware = CsrfViewMiddleware() + + def process_response(self, request, resp): + # We must do the response post-processing first, because that calls + # get_token(), which triggers a flag saying that the CSRF cookie needs + # to be sent (done in CsrfViewMiddleware.process_response) + resp2 = self.response_middleware.process_response(request, resp) + return self.view_middleware.process_response(request, resp2) + + def process_view(self, request, callback, callback_args, callback_kwargs): + return self.view_middleware.process_view(request, callback, callback_args, + callback_kwargs) + diff --git a/django/template/context.py b/django/template/context.py index 5fbdaf3a0d..f57a3aaa64 100644 --- a/django/template/context.py +++ b/django/template/context.py @@ -6,7 +6,7 @@ _standard_context_processors = None # We need the CSRF processor no matter what the user has in their settings, # because otherwise it is a security vulnerability, and we can't afford to leave # this to human error or failure to read migration instructions. -_builtin_context_processors = ('django.contrib.csrf.context_processors.csrf',) +_builtin_context_processors = ('django.core.context_processors.csrf',) class ContextPopException(Exception): "pop() has been called more times than push()" diff --git a/django/views/csrf.py b/django/views/csrf.py new file mode 100644 index 0000000000..dd8a8966b1 --- /dev/null +++ b/django/views/csrf.py @@ -0,0 +1,62 @@ +from django.http import HttpResponseForbidden +from django.template import Context, Template +from django.conf import settings + +# We include the template inline since we need to be able to reliably display +# this error message, especially for the sake of developers, and there isn't any +# other way of making it available independent of what is in the settings file. + +CSRF_FAILRE_TEMPLATE = """ + + + + + 403 Forbidden + + +

403 Forbidden

+

CSRF verification failed. Request aborted.

+ {% if DEBUG %} +

Help

+ {% if reason %} +

Reason given for failure:

+
+    {{ reason }}
+    
+ {% endif %} + +

In general, this can occur when there is a genuine Cross Site Request Forgery, or when + Django's + CSRF mechanism has not been used correctly. For POST forms, you need to + ensure:

+ +
    +
  • The view function uses RequestContext + for the template, instead of Context.
  • + +
  • In the template, there is a {% templatetag openblock %} csrf_token + {% templatetag closeblock %} template tag inside each POST form that + targets an internal URL.
  • +
+ +

You're seeing the help section of this page because you have DEBUG = + True in your Django settings file. Change that to False, + and only the initial error message will be displayed.

+ +

You can customize this page using the CSRF_FAILURE_VIEW setting.

+ + {% endif %} + + +""" + +def csrf_failure(request, reason=""): + """ + Default view used when request fails CSRF protection + """ + t = Template(CSRF_FAILRE_TEMPLATE) + c = Context({'DEBUG': settings.DEBUG, + 'reason': reason}) + return HttpResponseForbidden(t.render(c), mimetype='text/html') diff --git a/django/views/decorators/csrf.py b/django/views/decorators/csrf.py new file mode 100644 index 0000000000..b789872efe --- /dev/null +++ b/django/views/decorators/csrf.py @@ -0,0 +1,47 @@ +from django.middleware.csrf import CsrfViewMiddleware +from django.utils.decorators import decorator_from_middleware +try: + from functools import wraps +except ImportError: + from django.utils.functional import wraps # Python 2.3, 2.4 fallback. + +csrf_protect = decorator_from_middleware(CsrfViewMiddleware) +csrf_protect.__name__ = "csrf_protect" +csrf_protect.__doc__ = """ +This decorator adds CSRF protection in exactly the same way as +CsrfViewMiddleware, but it can be used on a per view basis. Using both, or +using the decorator multiple times, is harmless and efficient. +""" + +def csrf_response_exempt(view_func): + """ + Modifies a view function so that its response is exempt + from the post-processing of the CSRF middleware. + """ + def wrapped_view(*args, **kwargs): + resp = view_func(*args, **kwargs) + resp.csrf_exempt = True + return resp + return wraps(view_func)(wrapped_view) + +def csrf_view_exempt(view_func): + """ + Marks a view function as being exempt from CSRF view protection. + """ + # We could just do view_func.csrf_exempt = True, but decorators + # are nicer if they don't have side-effects, so we return a new + # function. + def wrapped_view(*args, **kwargs): + return view_func(*args, **kwargs) + wrapped_view.csrf_exempt = True + return wraps(view_func)(wrapped_view) + +def csrf_exempt(view_func): + """ + Marks a view function as being exempt from the CSRF checks + and post processing. + + This is the same as using both the csrf_view_exempt and + csrf_response_exempt decorators. + """ + return csrf_response_exempt(csrf_view_exempt(view_func)) diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 756b49920c..cdb012e5e4 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -19,6 +19,9 @@ their deprecation, as per the :ref:`Django deprecation policy token. ``CsrfMiddleware``, which combines ``CsrfResponseMiddleware`` and ``CsrfViewMiddleware``, is also deprecated. + * The old imports for CSRF functionality (``django.contrib.csrf.*``), + which moved to core in 1.2, will be removed. + * 2.0 * ``django.views.defaults.shortcut()``. This function has been moved to ``django.contrib.contenttypes.views.shortcut()`` as part of the diff --git a/docs/ref/contrib/csrf.txt b/docs/ref/contrib/csrf.txt index efd42a5a6d..0359599469 100644 --- a/docs/ref/contrib/csrf.txt +++ b/docs/ref/contrib/csrf.txt @@ -4,7 +4,7 @@ Cross Site Request Forgery protection ===================================== -.. module:: django.contrib.csrf +.. module:: django.middleware.csrf :synopsis: Protects against Cross Site Request Forgeries The CSRF middleware and template tag provides easy-to-use protection against @@ -37,13 +37,13 @@ How to use it To enable CSRF protection for your views, follow these steps: 1. Add the middleware - ``'django.contrib.csrf.middleware.CsrfViewMiddleware'`` to your list of + ``'django.middleware.csrf.CsrfViewMiddleware'`` to your list of middleware classes, :setting:`MIDDLEWARE_CLASSES`. (It should come before ``CsrfResponseMiddleware`` if that is being used, and before any view middleware that assume that CSRF attacks have been dealt with.) Alternatively, you can use the decorator - ``django.contrib.csrf.decorators.csrf_protect`` on particular views you + ``django.views.decorators.csrf.csrf_protect`` on particular views you want to protect. This is **not recommended** by itself, since if you forget to use it, you will have a security hole. The 'belt and braces' strategy of using both is fine, and will incur minimal overhead. @@ -57,11 +57,11 @@ To enable CSRF protection for your views, follow these steps: that would cause the CSRF token to be leaked, leading to a vulnerability. 3. In the corresponding view functions, ensure that the - ``'django.contrib.csrf.context_processors.csrf'`` context processor is + ``'django.core.context_processors.csrf'`` context processor is being used. Usually, this can be done in one of two ways: 1. Use RequestContext, which always uses - ``'django.contrib.csrf.context_processors.csrf'`` (no matter what your + ``'django.core.context_processors.csrf'`` (no matter what your TEMPLATE_CONTEXT_PROCESSORS setting). If you are using generic views or contrib apps, you are covered already, since these apps use RequestContext throughout. @@ -69,7 +69,7 @@ To enable CSRF protection for your views, follow these steps: 2. Manually import and use the processor to generate the CSRF token and add it to the template context. e.g.:: - from django.contrib.csrf.context_processors import csrf + from django.core.context_processors import csrf from django.shortcuts import render_to_response def my_view(request): @@ -96,7 +96,7 @@ as ``CsrfResponseMiddleware``, and it can be used by following these steps: 1. Follow step 1 above to install ``CsrfViewMiddleware``. - 2. Add ``'django.contrib.csrf.middleware.CsrfResponseMiddleware'`` to your + 2. Add ``'django.middleware.csrf.CsrfResponseMiddleware'`` to your :setting:`MIDDLEWARE_CLASSES` setting. ``CsrfResponseMiddleware`` needs to process the response before things @@ -140,6 +140,28 @@ enabled any CSRF protection. This section outlines the steps necessary for a smooth upgrade, without having to fix all the applications to use the new template tag method immediately. +First of all, the location of the middleware and related functions have +changed. There are backwards compatible stub files so that old imports will +continue to work for now, but they are deprecated and will be removed in Django +1.4. The following changes have been made: + + * Middleware have been moved to ``django.middleware.csrf`` + * Decorators have been moved to ``django.views.decorators.csrf`` + +====================================================== ============================================== + Old New +====================================================== ============================================== +django.contrib.csrf.middleware.CsrfMiddleware django.middleware.csrf.CsrfMiddleware +django.contrib.csrf.middleware.CsrfViewMiddleware django.middleware.csrf.CsrfViewMiddleware +django.contrib.csrf.middleware.CsrfResponseMiddleware django.middleware.csrf.CsrfResponseMiddleware +django.contrib.csrf.middleware.csrf_exempt django.views.decorators.csrf_exempt +django.contrib.csrf.middleware.csrf_view_exempt django.views.decorators.csrf_view_exempt +django.contrib.csrf.middleware.csrf_response_exempt django.views.decorators.csrf_response_exempt +====================================================== ============================================== + +You should update any imports, and also the paths in your +:setting:`MIDDLEWARE_CLASSES`. + If you have ``CsrfMiddleware`` in your :setting:`MIDDLEWARE_CLASSES`, you will now have a working installation with CSRF protection. It is recommended at this point that you replace ``CsrfMiddleware`` with its two components, @@ -186,9 +208,9 @@ Exceptions To manually exclude a view function from being handled by either of the two CSRF middleware, you can use the ``csrf_exempt`` decorator, found in the -``django.contrib.csrf.middleware`` module. For example:: +``django.views.decorators.csrf`` module. For example:: - from django.contrib.csrf.middleware import csrf_exempt + from django.views.decorators.csrf import csrf_exempt def my_view(request): return HttpResponse('Hello world') @@ -246,7 +268,7 @@ The CSRF protection is based on the following things: This cookie is set by ``CsrfViewMiddleware``. It is meant to be permanent, but since there is no way to set a cookie that never expires, it is sent with - every response that has called ``django.contrib.csrf.middleware.get_token()`` + every response that has called ``django.middleware.csrf.get_token()`` (the function used internally to retrieve the CSRF token). 2. A hidden form field with the name 'csrfmiddlewaretoken' present in all @@ -352,7 +374,7 @@ If you are using ``CsrfResponseMiddleware`` and your app creates HTML pages and forms in some unusual way, (e.g. it sends fragments of HTML in JavaScript document.write statements) you might bypass the filter that adds the hidden field to the form, in which case form submission will always fail. You should -use the template tag or :meth:`django.contrib.csrf.middleware.get_token` to get +use the template tag or :meth:`django.middleware.csrf.get_token` to get the CSRF token and ensure it is included when your form is submitted. Contrib and reusable apps diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt index ff51df9e8f..b0b26cc227 100644 --- a/docs/ref/middleware.txt +++ b/docs/ref/middleware.txt @@ -165,11 +165,11 @@ every incoming ``HttpRequest`` object. See :ref:`Authentication in Web requests CSRF protection middleware -------------------------- -.. module:: django.contrib.csrf.middleware +.. module:: django.middleware.csrf :synopsis: Middleware adding protection against Cross Site Request Forgeries. -.. class:: django.contrib.csrf.middleware.CsrfMiddleware +.. class:: django.middleware.csrf.CsrfMiddleware .. versionadded:: 1.0 diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 92c1012292..a7df35c2ac 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -171,7 +171,7 @@ accepted by accepted by a view served from another subdomain. CSRF_FAILURE_VIEW ----------------- -Default: ``'django.contrib.csrf.views.csrf_failure'`` +Default: ``'django.views.csrf.csrf_failure'`` A dotted path to the view function to be used when an incoming request is rejected by the CSRF protection. The function should have this signature:: @@ -789,7 +789,7 @@ Default:: ('django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.csrf.middleware.CsrfViewMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',) A tuple of middleware classes to use. See :ref:`topics-http-middleware`. diff --git a/docs/ref/templates/api.txt b/docs/ref/templates/api.txt index 6d91571228..1b6eeb7014 100644 --- a/docs/ref/templates/api.txt +++ b/docs/ref/templates/api.txt @@ -315,7 +315,7 @@ and return a dictionary of items to be merged into the context. By default, .. versionadded:: 1.2 In addition to these, ``RequestContext`` always uses - ``'django.contrib.csrf.context_processors.csrf'``. This is a security + ``'django.core.context_processors.csrf'``. This is a security related context processor required by the admin and other contrib apps, and, in case of accidental misconfiguration, it is deliberately hardcoded in and cannot be turned off by the :setting:`TEMPLATE_CONTEXT_PROCESSORS` setting. @@ -411,8 +411,8 @@ If :setting:`TEMPLATE_CONTEXT_PROCESSORS` contains this processor, every ``RequestContext`` will contain a variable ``MEDIA_URL``, providing the value of the :setting:`MEDIA_URL` setting. -django.contrib.csrf.context_processors.csrf -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +django.core.context_processors.csrf +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. versionadded:: 1.2 diff --git a/docs/releases/1.2-alpha.txt b/docs/releases/1.2-alpha.txt index abc701de7d..0806ed412d 100644 --- a/docs/releases/1.2-alpha.txt +++ b/docs/releases/1.2-alpha.txt @@ -18,6 +18,9 @@ changes that developers must be aware of: POST requests need to be written to work with the middleware. Instructions on how to do this are found in the CSRF docs. + * All of the CSRF has moved from contrib to core (with backwards compatible + imports in the old locations, which are deprecated). + LazyObject ---------- diff --git a/docs/topics/http/middleware.txt b/docs/topics/http/middleware.txt index f7bb73d6bd..c64ec7dc79 100644 --- a/docs/topics/http/middleware.txt +++ b/docs/topics/http/middleware.txt @@ -29,7 +29,7 @@ created by :djadmin:`django-admin.py startproject `:: MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.csrf.middleware.CsrfViewMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', ) diff --git a/tests/regressiontests/csrf_tests/__init__.py b/tests/regressiontests/csrf_tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/regressiontests/csrf_tests/models.py b/tests/regressiontests/csrf_tests/models.py new file mode 100644 index 0000000000..71abcc5198 --- /dev/null +++ b/tests/regressiontests/csrf_tests/models.py @@ -0,0 +1 @@ +# models.py file for tests to run. diff --git a/tests/regressiontests/csrf_tests/tests.py b/tests/regressiontests/csrf_tests/tests.py new file mode 100644 index 0000000000..5688293647 --- /dev/null +++ b/tests/regressiontests/csrf_tests/tests.py @@ -0,0 +1,324 @@ +# -*- coding: utf-8 -*- + +from django.test import TestCase +from django.http import HttpRequest, HttpResponse +from django.middleware.csrf import CsrfMiddleware, CsrfViewMiddleware +from django.views.decorators.csrf import csrf_exempt +from django.core.context_processors import csrf +from django.contrib.sessions.middleware import SessionMiddleware +from django.utils.importlib import import_module +from django.conf import settings +from django.template import RequestContext, Template + +# Response/views used for CsrfResponseMiddleware and CsrfViewMiddleware tests +def post_form_response(): + resp = HttpResponse(content=""" + +""", mimetype="text/html") + return resp + +def post_form_response_non_html(): + resp = post_form_response() + resp["Content-Type"] = "application/xml" + return resp + +def post_form_view(request): + """A view that returns a POST form (without a token)""" + return post_form_response() + +# Response/views used for template tag tests +def _token_template(): + return Template("{% csrf_token %}") + +def _render_csrf_token_template(req): + context = RequestContext(req, processors=[csrf]) + template = _token_template() + return template.render(context) + +def token_view(request): + """A view that uses {% csrf_token %}""" + return HttpResponse(_render_csrf_token_template(request)) + +def non_token_view_using_request_processor(request): + """ + A view that doesn't use the token, but does use the csrf view processor. + """ + context = RequestContext(request, processors=[csrf]) + template = Template("") + return HttpResponse(template.render(context)) + +class TestingHttpRequest(HttpRequest): + """ + A version of HttpRequest that allows us to change some things + more easily + """ + def is_secure(self): + return getattr(self, '_is_secure', False) + +class CsrfMiddlewareTest(TestCase): + _csrf_id = "1" + + # This is a valid session token for this ID and secret key. This was generated using + # the old code that we're to be backwards-compatible with. Don't use the CSRF code + # to generate this hash, or we're merely testing the code against itself and not + # checking backwards-compatibility. This is also the output of (echo -n test1 | md5sum). + _session_token = "5a105e8b9d40e1329780d62ea2265d8a" + _session_id = "1" + _secret_key_for_session_test= "test" + + def _get_GET_no_csrf_cookie_request(self): + return TestingHttpRequest() + + def _get_GET_csrf_cookie_request(self): + req = TestingHttpRequest() + req.COOKIES[settings.CSRF_COOKIE_NAME] = self._csrf_id + return req + + def _get_POST_csrf_cookie_request(self): + req = self._get_GET_csrf_cookie_request() + req.method = "POST" + return req + + def _get_POST_no_csrf_cookie_request(self): + req = self._get_GET_no_csrf_cookie_request() + req.method = "POST" + return req + + def _get_POST_request_with_token(self): + req = self._get_POST_csrf_cookie_request() + req.POST['csrfmiddlewaretoken'] = self._csrf_id + return req + + def _get_POST_session_request_with_token(self): + req = self._get_POST_no_csrf_cookie_request() + req.COOKIES[settings.SESSION_COOKIE_NAME] = self._session_id + req.POST['csrfmiddlewaretoken'] = self._session_token + return req + + def _get_POST_session_request_no_token(self): + req = self._get_POST_no_csrf_cookie_request() + req.COOKIES[settings.SESSION_COOKIE_NAME] = self._session_id + return req + + def _check_token_present(self, response, csrf_id=None): + self.assertContains(response, "name='csrfmiddlewaretoken' value='%s'" % (csrf_id or self._csrf_id)) + + # Check the post processing and outgoing cookie + def test_process_response_no_csrf_cookie(self): + """ + When no prior CSRF cookie exists, check that the cookie is created and a + token is inserted. + """ + req = self._get_GET_no_csrf_cookie_request() + CsrfMiddleware().process_view(req, post_form_view, (), {}) + + resp = post_form_response() + resp_content = resp.content # needed because process_response modifies resp + resp2 = CsrfMiddleware().process_response(req, resp) + + csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False) + self.assertNotEqual(csrf_cookie, False) + self.assertNotEqual(resp_content, resp2.content) + self._check_token_present(resp2, csrf_cookie.value) + # Check the Vary header got patched correctly + self.assert_('Cookie' in resp2.get('Vary','')) + + def test_process_response_no_csrf_cookie_view_only_get_token_used(self): + """ + When no prior CSRF cookie exists, check that the cookie is created, even + if only CsrfViewMiddleware is used. + """ + # This is checking that CsrfViewMiddleware has the cookie setting + # code. Most of the other tests use CsrfMiddleware. + req = self._get_GET_no_csrf_cookie_request() + # token_view calls get_token() indirectly + CsrfViewMiddleware().process_view(req, token_view, (), {}) + resp = token_view(req) + resp2 = CsrfViewMiddleware().process_response(req, resp) + + csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False) + self.assertNotEqual(csrf_cookie, False) + + def test_process_response_get_token_not_used(self): + """ + Check that if get_token() is not called, the view middleware does not + add a cookie. + """ + # This is important to make pages cacheable. Pages which do call + # get_token(), assuming they use the token, are not cacheable because + # the token is specific to the user + req = self._get_GET_no_csrf_cookie_request() + # non_token_view_using_request_processor does not call get_token(), but + # does use the csrf request processor. By using this, we are testing + # that the view processor is properly lazy and doesn't call get_token() + # until needed. + CsrfViewMiddleware().process_view(req, non_token_view_using_request_processor, (), {}) + resp = non_token_view_using_request_processor(req) + resp2 = CsrfViewMiddleware().process_response(req, resp) + + csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False) + self.assertEqual(csrf_cookie, False) + + def test_process_response_existing_csrf_cookie(self): + """ + Check that the token is inserted when a prior CSRF cookie exists + """ + req = self._get_GET_csrf_cookie_request() + CsrfMiddleware().process_view(req, post_form_view, (), {}) + + resp = post_form_response() + resp_content = resp.content # needed because process_response modifies resp + resp2 = CsrfMiddleware().process_response(req, resp) + self.assertNotEqual(resp_content, resp2.content) + self._check_token_present(resp2) + + def test_process_response_non_html(self): + """ + Check the the post-processor does nothing for content-types not in _HTML_TYPES. + """ + req = self._get_GET_no_csrf_cookie_request() + CsrfMiddleware().process_view(req, post_form_view, (), {}) + resp = post_form_response_non_html() + resp_content = resp.content # needed because process_response modifies resp + resp2 = CsrfMiddleware().process_response(req, resp) + self.assertEquals(resp_content, resp2.content) + + def test_process_response_exempt_view(self): + """ + Check that no post processing is done for an exempt view + """ + req = self._get_POST_csrf_cookie_request() + resp = csrf_exempt(post_form_view)(req) + resp_content = resp.content + resp2 = CsrfMiddleware().process_response(req, resp) + self.assertEquals(resp_content, resp2.content) + + # Check the request processing + def test_process_request_no_session_no_csrf_cookie(self): + """ + Check that if neither a CSRF cookie nor a session cookie are present, + the middleware rejects the incoming request. This will stop login CSRF. + """ + req = self._get_POST_no_csrf_cookie_request() + req2 = CsrfMiddleware().process_view(req, post_form_view, (), {}) + self.assertEquals(403, req2.status_code) + + def test_process_request_csrf_cookie_no_token(self): + """ + Check that if a CSRF cookie is present but no token, the middleware + rejects the incoming request. + """ + req = self._get_POST_csrf_cookie_request() + req2 = CsrfMiddleware().process_view(req, post_form_view, (), {}) + self.assertEquals(403, req2.status_code) + + def test_process_request_csrf_cookie_and_token(self): + """ + Check that if both a cookie and a token is present, the middleware lets it through. + """ + req = self._get_POST_request_with_token() + req2 = CsrfMiddleware().process_view(req, post_form_view, (), {}) + self.assertEquals(None, req2) + + def test_process_request_session_cookie_no_csrf_cookie_token(self): + """ + When no CSRF cookie exists, but the user has a session, check that a token + using the session cookie as a legacy CSRF cookie is accepted. + """ + orig_secret_key = settings.SECRET_KEY + settings.SECRET_KEY = self._secret_key_for_session_test + try: + req = self._get_POST_session_request_with_token() + req2 = CsrfMiddleware().process_view(req, post_form_view, (), {}) + self.assertEquals(None, req2) + finally: + settings.SECRET_KEY = orig_secret_key + + def test_process_request_session_cookie_no_csrf_cookie_no_token(self): + """ + Check that if a session cookie is present but no token and no CSRF cookie, + the request is rejected. + """ + req = self._get_POST_session_request_no_token() + req2 = CsrfMiddleware().process_view(req, post_form_view, (), {}) + self.assertEquals(403, req2.status_code) + + def test_process_request_csrf_cookie_no_token_exempt_view(self): + """ + Check that if a CSRF cookie is present and no token, but the csrf_exempt + decorator has been applied to the view, the middleware lets it through + """ + req = self._get_POST_csrf_cookie_request() + req2 = CsrfMiddleware().process_view(req, csrf_exempt(post_form_view), (), {}) + self.assertEquals(None, req2) + + def test_ajax_exemption(self): + """ + Check that AJAX requests are automatically exempted. + """ + req = self._get_POST_csrf_cookie_request() + req.META['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' + req2 = CsrfMiddleware().process_view(req, post_form_view, (), {}) + self.assertEquals(None, req2) + + # Tests for the template tag method + def test_token_node_no_csrf_cookie(self): + """ + Check that CsrfTokenNode works when no CSRF cookie is set + """ + req = self._get_GET_no_csrf_cookie_request() + resp = token_view(req) + self.assertEquals(u"", resp.content) + + def test_token_node_with_csrf_cookie(self): + """ + Check that CsrfTokenNode works when a CSRF cookie is set + """ + req = self._get_GET_csrf_cookie_request() + CsrfViewMiddleware().process_view(req, token_view, (), {}) + resp = token_view(req) + self._check_token_present(resp) + + def test_token_node_with_new_csrf_cookie(self): + """ + Check that CsrfTokenNode works when a CSRF cookie is created by + the middleware (when one was not already present) + """ + req = self._get_GET_no_csrf_cookie_request() + CsrfViewMiddleware().process_view(req, token_view, (), {}) + resp = token_view(req) + resp2 = CsrfViewMiddleware().process_response(req, resp) + csrf_cookie = resp2.cookies[settings.CSRF_COOKIE_NAME] + self._check_token_present(resp, csrf_id=csrf_cookie.value) + + def test_response_middleware_without_view_middleware(self): + """ + Check that CsrfResponseMiddleware finishes without error if the view middleware + has not been called, as is the case if a request middleware returns a response. + """ + req = self._get_GET_no_csrf_cookie_request() + resp = post_form_view(req) + CsrfMiddleware().process_response(req, resp) + + def test_https_bad_referer(self): + """ + Test that a POST HTTPS request with a bad referer is rejected + """ + req = self._get_POST_request_with_token() + req._is_secure = True + req.META['HTTP_HOST'] = 'www.example.com' + req.META['HTTP_REFERER'] = 'https://www.evil.org/somepage' + req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {}) + self.assertNotEqual(None, req2) + self.assertEquals(403, req2.status_code) + + def test_https_good_referer(self): + """ + Test that a POST HTTPS request with a good referer is accepted + """ + req = self._get_POST_request_with_token() + req._is_secure = True + req.META['HTTP_HOST'] = 'www.example.com' + req.META['HTTP_REFERER'] = 'https://www.example.com/somepage' + req2 = CsrfViewMiddleware().process_view(req, post_form_view, (), {}) + self.assertEquals(None, req2) -- cgit v1.3