diff --git a/AUTHORS b/AUTHORS index 4ba21fe0d9..53c1769cc7 100644 --- a/AUTHORS +++ b/AUTHORS @@ -470,6 +470,8 @@ answer newbie questions, and generally made Django that much better: Gasper Zejn Jarek Zgoda Cheng Zhang + Glenn + bthomas A big THANK YOU goes to: diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 99fc72e468..62c7dd90c2 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -300,6 +300,7 @@ DEFAULT_INDEX_TABLESPACE = '' MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.csrf.middleware.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', # 'django.middleware.http.ConditionalGetMiddleware', # 'django.middleware.gzip.GZipMiddleware', @@ -374,6 +375,18 @@ LOGIN_REDIRECT_URL = '/accounts/profile/' # The number of days a password reset link is valid for PASSWORD_RESET_TIMEOUT_DAYS = 3 +######## +# CSRF # +######## + +# 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' + +# Name and domain for CSRF cookie. +CSRF_COOKIE_NAME = 'csrftoken' +CSRF_COOKIE_DOMAIN = None + ########### # TESTING # ########### diff --git a/django/conf/project_template/settings.py b/django/conf/project_template/settings.py index bbf005ddea..f83f3d505a 100644 --- a/django/conf/project_template/settings.py +++ b/django/conf/project_template/settings.py @@ -60,6 +60,7 @@ TEMPLATE_LOADERS = ( MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.csrf.middleware.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', ) diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 3144a22a2a..c702e87340 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -6,6 +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.core.exceptions import PermissionDenied from django.db import models, transaction from django.db.models.fields import BLANK_CHOICE_DASH @@ -701,6 +702,8 @@ class ModelAdmin(BaseModelAdmin): else: return HttpResponseRedirect(".") + @csrf_protect + @transaction.commit_on_success def add_view(self, request, form_url='', extra_context=None): "The 'add' admin view for this model." model = self.model @@ -782,8 +785,9 @@ class ModelAdmin(BaseModelAdmin): } context.update(extra_context or {}) return self.render_change_form(request, context, form_url=form_url, add=True) - add_view = transaction.commit_on_success(add_view) + @csrf_protect + @transaction.commit_on_success def change_view(self, request, object_id, extra_context=None): "The 'change' admin view for this model." model = self.model @@ -871,8 +875,8 @@ class ModelAdmin(BaseModelAdmin): } context.update(extra_context or {}) return self.render_change_form(request, context, change=True, obj=obj) - change_view = transaction.commit_on_success(change_view) + @csrf_protect def changelist_view(self, request, extra_context=None): "The 'change list' admin view for this model." from django.contrib.admin.views.main import ChangeList, ERROR_FLAG @@ -985,6 +989,7 @@ class ModelAdmin(BaseModelAdmin): 'admin/change_list.html' ], context, context_instance=context_instance) + @csrf_protect def delete_view(self, request, object_id, extra_context=None): "The 'delete' admin view for this model." opts = self.model._meta diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index 2e81cbb8b9..d686540e56 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -3,6 +3,8 @@ 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.db.models.base import ModelBase from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import reverse @@ -186,6 +188,9 @@ class AdminSite(object): return view(request, *args, **kwargs) if not cacheable: inner = never_cache(inner) + # We add csrf_protect here so this function can be used as a utility + # function for any view, without having to repeat 'csrf_protect'. + inner = csrf_response_exempt(csrf_protect(inner)) return update_wrapper(inner, view) def get_urls(self): diff --git a/django/contrib/admin/templates/admin/auth/user/change_password.html b/django/contrib/admin/templates/admin/auth/user/change_password.html index d28dd0f45c..11414d1465 100644 --- a/django/contrib/admin/templates/admin/auth/user/change_password.html +++ b/django/contrib/admin/templates/admin/auth/user/change_password.html @@ -15,7 +15,7 @@ {% endif %}{% endblock %} {% block content %}
-
{% block form_top %}{% endblock %} +{% csrf_token %}{% block form_top %}{% endblock %}
{% if is_popup %}{% endif %} {% if form.errors %} diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html index f645d65a0f..c5ac729c7e 100644 --- a/django/contrib/admin/templates/admin/change_form.html +++ b/django/contrib/admin/templates/admin/change_form.html @@ -29,7 +29,7 @@ {% endif %}{% endif %} {% endblock %} -{% block form_top %}{% endblock %} +{% csrf_token %}{% block form_top %}{% endblock %}
{% if is_popup %}{% endif %} {% if save_on_top %}{% submit_row %}{% endif %} diff --git a/django/contrib/admin/templates/admin/change_list.html b/django/contrib/admin/templates/admin/change_list.html index 31bf7bd29a..20b2eff060 100644 --- a/django/contrib/admin/templates/admin/change_list.html +++ b/django/contrib/admin/templates/admin/change_list.html @@ -68,7 +68,7 @@ {% endif %} {% endblock %} - + {% csrf_token %} {% if cl.formset %} {{ cl.formset.management_form }} {% endif %} diff --git a/django/contrib/admin/templates/admin/delete_confirmation.html b/django/contrib/admin/templates/admin/delete_confirmation.html index 42802f57bc..65e73c922d 100644 --- a/django/contrib/admin/templates/admin/delete_confirmation.html +++ b/django/contrib/admin/templates/admin/delete_confirmation.html @@ -22,7 +22,7 @@ {% else %}

{% blocktrans with object as escaped_object %}Are you sure you want to delete the {{ object_name }} "{{ escaped_object }}"? All of the following related items will be deleted:{% endblocktrans %}

    {{ deleted_objects|unordered_list }}
- + {% csrf_token %}
diff --git a/django/contrib/admin/templates/admin/delete_selected_confirmation.html b/django/contrib/admin/templates/admin/delete_selected_confirmation.html index 5550b73e2e..7f4fbc5726 100644 --- a/django/contrib/admin/templates/admin/delete_selected_confirmation.html +++ b/django/contrib/admin/templates/admin/delete_selected_confirmation.html @@ -23,7 +23,7 @@ {% for deleteable_object in deletable_objects %}
    {{ deleteable_object|unordered_list }}
{% endfor %} - + {% csrf_token %}
{% for obj in queryset %} diff --git a/django/contrib/admin/templates/admin/login.html b/django/contrib/admin/templates/admin/login.html index d162e5a9fa..876c4b0327 100644 --- a/django/contrib/admin/templates/admin/login.html +++ b/django/contrib/admin/templates/admin/login.html @@ -14,7 +14,7 @@

{{ error_message }}

{% endif %}
- +{% csrf_token %}
diff --git a/django/contrib/admin/templates/admin/template_validator.html b/django/contrib/admin/templates/admin/template_validator.html index d221807486..9a139c5d49 100644 --- a/django/contrib/admin/templates/admin/template_validator.html +++ b/django/contrib/admin/templates/admin/template_validator.html @@ -4,7 +4,7 @@
- +{% csrf_token %} {% if form.errors %}

Your template had {{ form.errors|length }} error{{ form.errors|pluralize }}:

diff --git a/django/contrib/admin/templates/registration/password_change_form.html b/django/contrib/admin/templates/registration/password_change_form.html index c13c7f7040..6d7a6609de 100644 --- a/django/contrib/admin/templates/registration/password_change_form.html +++ b/django/contrib/admin/templates/registration/password_change_form.html @@ -11,7 +11,7 @@

{% trans "Please enter your old password, for security's sake, and then enter your new password twice so we can verify you typed it in correctly." %}

- +{% csrf_token %} {{ form.old_password.errors }}

{{ form.old_password }}

diff --git a/django/contrib/admin/templates/registration/password_reset_confirm.html b/django/contrib/admin/templates/registration/password_reset_confirm.html index 049ee625a9..df9cf1b316 100644 --- a/django/contrib/admin/templates/registration/password_reset_confirm.html +++ b/django/contrib/admin/templates/registration/password_reset_confirm.html @@ -13,7 +13,7 @@

{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}

- +{% csrf_token %} {{ form.new_password1.errors }}

{{ form.new_password1 }}

{{ form.new_password2.errors }} diff --git a/django/contrib/admin/templates/registration/password_reset_form.html b/django/contrib/admin/templates/registration/password_reset_form.html index 704066c68a..d3a128428a 100644 --- a/django/contrib/admin/templates/registration/password_reset_form.html +++ b/django/contrib/admin/templates/registration/password_reset_form.html @@ -11,7 +11,7 @@

{% trans "Forgotten your password? Enter your e-mail address below, and we'll e-mail instructions for setting a new one." %}

- +{% csrf_token %} {{ form.email.errors }}

{{ form.email }}

diff --git a/django/contrib/auth/views.py b/django/contrib/auth/views.py index 49a554a59d..9d36710211 100644 --- a/django/contrib/auth/views.py +++ b/django/contrib/auth/views.py @@ -4,6 +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.core.urlresolvers import reverse from django.shortcuts import render_to_response, get_object_or_404 from django.contrib.sites.models import Site, RequestSite @@ -14,6 +15,8 @@ from django.utils.translation import ugettext as _ from django.contrib.auth.models import User from django.views.decorators.cache import never_cache +@csrf_protect +@never_cache def login(request, template_name='registration/login.html', redirect_field_name=REDIRECT_FIELD_NAME, authentication_form=AuthenticationForm): @@ -43,7 +46,6 @@ def login(request, template_name='registration/login.html', 'site': current_site, 'site_name': current_site.name, }, context_instance=RequestContext(request)) -login = never_cache(login) def logout(request, next_page=None, template_name='registration/logged_out.html', redirect_field_name=REDIRECT_FIELD_NAME): "Logs out the user and displays 'You are logged out' message." @@ -80,6 +82,7 @@ def redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_N # prompts for a new password # - password_reset_complete shows a success message for the above +@csrf_protect def password_reset(request, is_admin_site=False, template_name='registration/password_reset_form.html', email_template_name='registration/password_reset_email.html', password_reset_form=PasswordResetForm, token_generator=default_token_generator, @@ -109,6 +112,7 @@ def password_reset(request, is_admin_site=False, template_name='registration/pas def password_reset_done(request, template_name='registration/password_reset_done.html'): return render_to_response(template_name, context_instance=RequestContext(request)) +# Doesn't need csrf_protect since no-one can guess the URL def password_reset_confirm(request, uidb36=None, token=None, template_name='registration/password_reset_confirm.html', token_generator=default_token_generator, set_password_form=SetPasswordForm, post_reset_redirect=None): @@ -146,6 +150,8 @@ def password_reset_complete(request, template_name='registration/password_reset_ return render_to_response(template_name, context_instance=RequestContext(request, {'login_url': settings.LOGIN_URL})) +@csrf_protect +@login_required def password_change(request, template_name='registration/password_change_form.html', post_change_redirect=None, password_change_form=PasswordChangeForm): if post_change_redirect is None: @@ -160,7 +166,6 @@ def password_change(request, template_name='registration/password_change_form.ht return render_to_response(template_name, { 'form': form, }, context_instance=RequestContext(request)) -password_change = login_required(password_change) def password_change_done(request, template_name='registration/password_change_done.html'): return render_to_response(template_name, context_instance=RequestContext(request)) diff --git a/django/contrib/comments/templates/comments/approve.html b/django/contrib/comments/templates/comments/approve.html index a4306a6fc2..1a3a3fd80c 100644 --- a/django/contrib/comments/templates/comments/approve.html +++ b/django/contrib/comments/templates/comments/approve.html @@ -6,7 +6,7 @@ {% block content %}

{% trans "Really make this comment public?" %}

{{ comment|linebreaks }}
-
+ {% csrf_token %} {% if next %}{% endif %}

or cancel diff --git a/django/contrib/comments/templates/comments/delete.html b/django/contrib/comments/templates/comments/delete.html index 7d73eac979..5ff2add9c5 100644 --- a/django/contrib/comments/templates/comments/delete.html +++ b/django/contrib/comments/templates/comments/delete.html @@ -6,7 +6,7 @@ {% block content %}

{% trans "Really remove this comment?" %}

{{ comment|linebreaks }}
- + {% csrf_token %} {% if next %}{% endif %}

or cancel diff --git a/django/contrib/comments/templates/comments/flag.html b/django/contrib/comments/templates/comments/flag.html index 08dbe0b0b0..0b9ab1ccb2 100644 --- a/django/contrib/comments/templates/comments/flag.html +++ b/django/contrib/comments/templates/comments/flag.html @@ -6,7 +6,7 @@ {% block content %}

{% trans "Really flag this comment?" %}

{{ comment|linebreaks }}
- + {% csrf_token %} {% if next %}{% endif %}

or cancel diff --git a/django/contrib/comments/templates/comments/form.html b/django/contrib/comments/templates/comments/form.html index d8e248372f..30f031128c 100644 --- a/django/contrib/comments/templates/comments/form.html +++ b/django/contrib/comments/templates/comments/form.html @@ -1,5 +1,5 @@ {% load comments i18n %} - +{% csrf_token %} {% if next %}{% endif %} {% for field in form %} {% if field.is_hidden %} diff --git a/django/contrib/comments/templates/comments/preview.html b/django/contrib/comments/templates/comments/preview.html index d3884575f5..1b072a76f0 100644 --- a/django/contrib/comments/templates/comments/preview.html +++ b/django/contrib/comments/templates/comments/preview.html @@ -5,7 +5,7 @@ {% block content %} {% load comments %} - + {% csrf_token %} {% if next %}{% endif %} {% if form.errors %}

{% blocktrans count form.errors|length as counter %}Please correct the error below{% plural %}Please correct the errors below{% endblocktrans %}

diff --git a/django/contrib/comments/views/comments.py b/django/contrib/comments/views/comments.py index 89a3dd9bba..ada7e9c77e 100644 --- a/django/contrib/comments/views/comments.py +++ b/django/contrib/comments/views/comments.py @@ -10,6 +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 class CommentPostBadRequest(http.HttpResponseBadRequest): """ @@ -22,6 +23,8 @@ class CommentPostBadRequest(http.HttpResponseBadRequest): if settings.DEBUG: self.content = render_to_string("comments/400-debug.html", {"why": why}) +@csrf_protect +@require_POST def post_comment(request, next=None): """ Post a comment. @@ -116,8 +119,6 @@ def post_comment(request, next=None): return next_redirect(data, next, comment_done, c=comment._get_pk_val()) -post_comment = require_POST(post_comment) - comment_done = confirmation_view( template = "comments/posted.html", doc = """Display a "comment was posted" success page.""" diff --git a/django/contrib/comments/views/moderation.py b/django/contrib/comments/views/moderation.py index d47fa8b4e7..76db326c31 100644 --- a/django/contrib/comments/views/moderation.py +++ b/django/contrib/comments/views/moderation.py @@ -5,7 +5,9 @@ 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 +@csrf_protect @login_required def flag(request, comment_id, next=None): """ @@ -30,6 +32,7 @@ def flag(request, comment_id, next=None): template.RequestContext(request) ) +@csrf_protect @permission_required("comments.can_moderate") def delete(request, comment_id, next=None): """ @@ -56,6 +59,7 @@ def delete(request, comment_id, next=None): template.RequestContext(request) ) +@csrf_protect @permission_required("comments.can_moderate") def approve(request, comment_id, next=None): """ diff --git a/django/contrib/csrf/context_processors.py b/django/contrib/csrf/context_processors.py new file mode 100644 index 0000000000..b78030a0b2 --- /dev/null +++ b/django/contrib/csrf/context_processors.py @@ -0,0 +1,20 @@ +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 new file mode 100644 index 0000000000..67e33bce5c --- /dev/null +++ b/django/contrib/csrf/decorators.py @@ -0,0 +1,10 @@ +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 40cbcf502b..daee12379e 100644 --- a/django/contrib/csrf/middleware.py +++ b/django/contrib/csrf/middleware.py @@ -5,94 +5,213 @@ This module provides a middleware that implements protection against request forgeries from other sites. """ -import re 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.http import HttpResponseForbidden +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 -_ERROR_MSG = mark_safe('

403 Forbidden

Cross Site Request Forgery detected. Request aborted.

') - _POST_FORM_RE = \ re.compile(r'(]*\bmethod\s*=\s*(\'|"|)POST(\'|"|)\b[^>]*>)', re.IGNORECASE) _HTML_TYPES = ('text/html', 'application/xhtml+xml') -def _make_token(session_id): +# 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 an active session. + 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(callback, 'csrf_exempt', False): - return None + 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(): - return None + # .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() - try: - session_id = request.COOKIES[settings.SESSION_COOKIE_NAME] - except KeyError: - # No session, no check required - return None + 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"] - csrf_token = _make_token(session_id) # check incoming token - try: - request_csrf_token = request.POST['csrfmiddlewaretoken'] - except KeyError: - return HttpResponseForbidden(_ERROR_MSG) - + request_csrf_token = request.POST.get('csrfmiddlewaretoken', None) if request_csrf_token != csrf_token: - return HttpResponseForbidden(_ERROR_MSG) + return reject("CSRF token missing or incorrect.") - return None + 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): """ - Middleware that post-processes a response to add a - csrfmiddlewaretoken if the response/request have an active - session. + 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 - csrf_token = None - try: - # This covers a corner case in which the outgoing response - # both contains a form and sets a session cookie. This - # really should not be needed, since it is best if views - # that create a new session (login pages) also do a - # redirect, as is done by all such view functions in - # Django. - cookie = response.cookies[settings.SESSION_COOKIE_NAME] - csrf_token = _make_token(cookie.value) - except KeyError: - # Normal case - look for existing session cookie - try: - session_id = request.COOKIES[settings.SESSION_COOKIE_NAME] - csrf_token = _make_token(session_id) - except KeyError: - # no incoming or outgoing cookie - pass - - if csrf_token is not None and \ - response['Content-Type'].split(';')[0] in _HTML_TYPES: + 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('')) + itertools.repeat('')) def add_csrf_field(match): """Returns the matched tag plus the added element""" return mark_safe(match.group() + "
" + \ @@ -103,34 +222,43 @@ class CsrfResponseMiddleware(object): # 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 is we assume that + # 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(CsrfViewMiddleware, CsrfResponseMiddleware): - """Django middleware that adds protection against Cross Site +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. - In the list of middlewares, SessionMiddleware is required, and - must come after this middleware. CsrfMiddleWare must come after - compression middleware. - - If a session ID cookie is present, it is hashed with the - SECRET_KEY setting to create an authentication token. This token - is added to all outgoing POST forms and is expected on all - incoming POST requests that have a session ID cookie. - - If you are setting cookies directly, instead of using Django's - session framework, this middleware will not work. - - CsrfMiddleWare is composed of two middleware, CsrfViewMiddleware - and CsrfResponseMiddleware which can be used independently. + 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. """ - pass + # 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): """ diff --git a/django/contrib/csrf/tests.py b/django/contrib/csrf/tests.py index 3c533a01e6..14015736c4 100644 --- a/django/contrib/csrf/tests.py +++ b/django/contrib/csrf/tests.py @@ -1,144 +1,323 @@ # -*- coding: utf-8 -*- from django.test import TestCase -from django.http import HttpRequest, HttpResponse, HttpResponseForbidden -from django.contrib.csrf.middleware import CsrfMiddleware, _make_token, csrf_exempt +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 test_view(request): +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_session_request(self): - return HttpRequest() + def _get_GET_no_csrf_cookie_request(self): + return TestingHttpRequest() - def _get_GET_session_request(self): - req = self._get_GET_no_session_request() - req.COOKIES[settings.SESSION_COOKIE_NAME] = self._session_id + def _get_GET_csrf_cookie_request(self): + req = TestingHttpRequest() + req.COOKIES[settings.CSRF_COOKIE_NAME] = self._csrf_id return req - def _get_POST_session_request(self): - req = self._get_GET_session_request() + def _get_POST_csrf_cookie_request(self): + req = self._get_GET_csrf_cookie_request() req.method = "POST" return req - def _get_POST_no_session_request(self): - req = self._get_GET_no_session_request() + 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_session_request() - req.POST['csrfmiddlewaretoken'] = _make_token(self._session_id) + 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_form_response(self): - return post_form_response() + 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 _get_new_session_response(self): - resp = self._get_post_form_response() - resp.cookies[settings.SESSION_COOKIE_NAME] = self._session_id - return resp + def _check_token_present(self, response, csrf_id=None): + self.assertContains(response, "name='csrfmiddlewaretoken' value='%s'" % (csrf_id or self._csrf_id)) - def _check_token_present(self, response): - self.assertContains(response, "name='csrfmiddlewaretoken' value='%s'" % _make_token(self._session_id)) - - def get_view(self): - return test_view - - # Check the post processing - def test_process_response_no_session(self): + # Check the post processing and outgoing cookie + def test_process_response_no_csrf_cookie(self): """ - Check the post-processor does nothing if no session active + When no prior CSRF cookie exists, check that the cookie is created and a + token is inserted. """ - req = self._get_GET_no_session_request() - resp = self._get_post_form_response() + 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_existing_session(self): - """ - Check that the token is inserted if there is an existing session - """ - req = self._get_GET_session_request() - resp = self._get_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_new_session(self): - """ - Check that the token is inserted if there is a new session being started - """ - req = self._get_GET_no_session_request() # no session in request - resp = self._get_new_session_response() # but new session started - 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_exempt_view(self): """ Check that no post processing is done for an exempt view """ - req = self._get_POST_session_request() - resp = csrf_exempt(self.get_view())(req) + 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(self): + def test_process_request_no_session_no_csrf_cookie(self): """ - Check that if no session is present, the middleware does nothing. - to the incoming request. + 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_session_request() - req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {}) + 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_no_token(self): + def test_process_request_session_cookie_no_csrf_cookie_token(self): """ - Check that if a session is present but no token, we get a 'forbidden' + 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. """ - req = self._get_POST_session_request() - req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {}) - self.assertEquals(HttpResponseForbidden, req2.__class__) + 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_and_token(self): + def test_process_request_session_cookie_no_csrf_cookie_no_token(self): """ - Check that if a session is present and a token, the middleware lets it through + 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_with_token() - req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {}) - self.assertEquals(None, req2) + 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_session_no_token_exempt_view(self): + def test_process_request_csrf_cookie_no_token_exempt_view(self): """ - Check that if a session is present and no token, but the csrf_exempt + 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_session_request() - req2 = CsrfMiddleware().process_view(req, csrf_exempt(self.get_view()), (), {}) + 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_session_request() + req = self._get_POST_csrf_cookie_request() req.META['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' - req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {}) + 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 new file mode 100644 index 0000000000..dd8a8966b1 --- /dev/null +++ b/django/contrib/csrf/views.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/contrib/formtools/templates/formtools/form.html b/django/contrib/formtools/templates/formtools/form.html index 194bbdd675..2f2de1f637 100644 --- a/django/contrib/formtools/templates/formtools/form.html +++ b/django/contrib/formtools/templates/formtools/form.html @@ -4,7 +4,7 @@ {% if form.errors %}

Please correct the following errors

{% else %}

Submit

{% endif %} -
+{% csrf_token %} {{ form }}
diff --git a/django/contrib/formtools/templates/formtools/preview.html b/django/contrib/formtools/templates/formtools/preview.html index c53ce91724..eb88b1ec2e 100644 --- a/django/contrib/formtools/templates/formtools/preview.html +++ b/django/contrib/formtools/templates/formtools/preview.html @@ -15,7 +15,7 @@

Security hash: {{ hash_value }}

- +{% csrf_token %} {% for field in form %}{{ field.as_hidden }} {% endfor %} @@ -25,7 +25,7 @@

Or edit it again

- +{% csrf_token %} {{ form }}
diff --git a/django/contrib/formtools/tests.py b/django/contrib/formtools/tests.py index 86d40b963b..bc65a60fbe 100644 --- a/django/contrib/formtools/tests.py +++ b/django/contrib/formtools/tests.py @@ -147,15 +147,18 @@ class WizardPageTwoForm(forms.Form): class WizardClass(wizard.FormWizard): def render_template(self, *args, **kw): - return "" + return http.HttpResponse("") def done(self, request, cleaned_data): return http.HttpResponse(success_string) -class DummyRequest(object): +class DummyRequest(http.HttpRequest): def __init__(self, POST=None): + super(DummyRequest, self).__init__() self.method = POST and "POST" or "GET" - self.POST = POST + if POST is not None: + self.POST.update(POST) + self._dont_enforce_csrf_checks = True class WizardTests(TestCase): def test_step_starts_at_zero(self): diff --git a/django/contrib/formtools/wizard.py b/django/contrib/formtools/wizard.py index b075628c49..60fe314217 100644 --- a/django/contrib/formtools/wizard.py +++ b/django/contrib/formtools/wizard.py @@ -14,6 +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 class FormWizard(object): # Dictionary of extra template context variables. @@ -44,6 +45,7 @@ class FormWizard(object): # hook methods might alter self.form_list. return len(self.form_list) + @csrf_protect def __call__(self, request, *args, **kwargs): """ Main method that does all the hard work, conforming to the Django view diff --git a/django/template/context.py b/django/template/context.py index 1c43387468..5fbdaf3a0d 100644 --- a/django/template/context.py +++ b/django/template/context.py @@ -1,7 +1,12 @@ from django.core.exceptions import ImproperlyConfigured from django.utils.importlib import import_module +# Cache of actual callables. _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',) class ContextPopException(Exception): "pop() has been called more times than push()" @@ -75,7 +80,10 @@ def get_standard_processors(): global _standard_context_processors if _standard_context_processors is None: processors = [] - for path in settings.TEMPLATE_CONTEXT_PROCESSORS: + collect = [] + collect.extend(_builtin_context_processors) + collect.extend(settings.TEMPLATE_CONTEXT_PROCESSORS) + for path in collect: i = path.rfind('.') module, attr = path[:i], path[i+1:] try: diff --git a/django/template/defaulttags.py b/django/template/defaulttags.py index de746997ab..6d57cdeef8 100644 --- a/django/template/defaulttags.py +++ b/django/template/defaulttags.py @@ -37,6 +37,23 @@ class CommentNode(Node): def render(self, context): return '' +class CsrfTokenNode(Node): + def render(self, context): + csrf_token = context.get('csrf_token', None) + if csrf_token: + if csrf_token == 'NOTPROVIDED': + return mark_safe(u"") + else: + return mark_safe(u"
" % (csrf_token)) + else: + # It's very probable that the token is missing because of + # misconfiguration, so we raise a warning + from django.conf import settings + if settings.DEBUG: + import warnings + warnings.warn("A {% csrf_token %} was used in a template, but the context did not provide the value. This is usually caused by not using RequestContext.") + return u'' + class CycleNode(Node): def __init__(self, cyclevars, variable_name=None): self.cycle_iter = itertools_cycle(cyclevars) @@ -523,6 +540,10 @@ def cycle(parser, token): return node cycle = register.tag(cycle) +def csrf_token(parser, token): + return CsrfTokenNode() +register.tag(csrf_token) + def debug(parser, token): """ Outputs a whole load of debugging information, including the current diff --git a/django/test/client.py b/django/test/client.py index 7d50ccb326..63ad1c1d3a 100644 --- a/django/test/client.py +++ b/django/test/client.py @@ -66,6 +66,11 @@ class ClientHandler(BaseHandler): signals.request_started.send(sender=self.__class__) try: request = WSGIRequest(environ) + # sneaky little hack so that we can easily get round + # CsrfViewMiddleware. This makes life easier, and is probably + # required for backwards compatibility with external tests against + # admin views. + request._dont_enforce_csrf_checks = True response = self.get_response(request) # Apply response middleware. diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 7e7f4c6338..756b49920c 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -13,6 +13,12 @@ their deprecation, as per the :ref:`Django deprecation policy hooking up admin URLs. This has been deprecated since the 1.1 release. + * 1.4 + * ``CsrfResponseMiddleware``. This has been deprecated since the 1.2 + release, in favour of the template tag method for inserting the CSRF + token. ``CsrfMiddleware``, which combines ``CsrfResponseMiddleware`` + and ``CsrfViewMiddleware``, is also deprecated. + * 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/intro/tutorial04.txt b/docs/intro/tutorial04.txt index bcc45f93c1..394fc25ea8 100644 --- a/docs/intro/tutorial04.txt +++ b/docs/intro/tutorial04.txt @@ -21,6 +21,7 @@ tutorial, so that the template contains an HTML ```` element: {% if error_message %}

{{ error_message }}

{% endif %} + {% csrf_token %} {% for choice in poll.choice_set.all %}
@@ -46,6 +47,28 @@ A quick rundown: * ``forloop.counter`` indicates how many times the :ttag:`for` tag has gone through its loop + * Since we are creating a POST form (which can have the effect of modifying + data), we unfortunately need to worry about Cross Site Request Forgeries. + Thankfully, you don't have to worry too hard, because Django comes with + very easy-to-use system for protecting against it. In short, all POST + forms that are targetted at internal URLs need the ``{% csrf_token %}`` + template tag adding. + +The ``{% csrf_token %}`` tag requires information from the request object, which +is not normally accessible from within the template context. To fix this, a +small adjustment needs to be made to the ``detail`` view, so that it looks like +the following:: + + from django.template import RequestContext + # ... + def detail(request, poll_id): + p = get_object_or_404(Poll, pk=poll_id) + return render_to_response('polls/detail.html', {'poll': p}, + context_instance=RequestContext(request)) + +The details of how this works are explained in the documentation for +:ref:`RequestContext `. + Now, let's create a Django view that handles the submitted data and does something with it. Remember, in :ref:`Tutorial 3 `, we created a URLconf for the polls application that includes this line:: @@ -58,6 +81,7 @@ create a real version. Add the following to ``mysite/polls/views.py``:: from django.shortcuts import get_object_or_404, render_to_response from django.http import HttpResponseRedirect, HttpResponse from django.core.urlresolvers import reverse + from django.template import RequestContext from mysite.polls.models import Choice, Poll # ... def vote(request, poll_id): @@ -69,7 +93,7 @@ create a real version. Add the following to ``mysite/polls/views.py``:: return render_to_response('polls/detail.html', { 'poll': p, 'error_message': "You didn't select a choice.", - }) + }, context_instance=RequestContext(request)) else: selected_choice.votes += 1 selected_choice.save() diff --git a/docs/ref/contrib/csrf.txt b/docs/ref/contrib/csrf.txt index cbe55dc38a..efd42a5a6d 100644 --- a/docs/ref/contrib/csrf.txt +++ b/docs/ref/contrib/csrf.txt @@ -7,46 +7,186 @@ Cross Site Request Forgery protection .. module:: django.contrib.csrf :synopsis: Protects against Cross Site Request Forgeries -The CsrfMiddleware class provides easy-to-use protection against +The CSRF middleware and template tag provides easy-to-use protection against `Cross Site Request Forgeries`_. This type of attack occurs when a malicious -Web site creates a link or form button that is intended to perform some action -on your Web site, using the credentials of a logged-in user who is tricked -into clicking on the link in their browser. +Web site contains a link, a form button or some javascript that is intended to +perform some action on your Web site, using the credentials of a logged-in user +who visits the malicious site in their browser. A related type of attack, +'login CSRF', where an attacking site tricks a user's browser into logging into +a site with someone else's credentials, is also covered. -The first defense against CSRF attacks is to ensure that GET requests -are side-effect free. POST requests can then be protected by adding this -middleware into your list of installed middleware. +The first defense against CSRF attacks is to ensure that GET requests are +side-effect free. POST requests can then be protected by following the steps +below. + +.. versionadded:: 1.2 + The 'contrib' apps, including the admin, use the functionality described + here. Because it is security related, a few things have been added to core + functionality to allow this to happen without any required upgrade steps. .. _Cross Site Request Forgeries: http://www.squarefree.com/securitytips/web-developers.html#CSRF How to use it ============= -Add the middleware ``'django.contrib.csrf.middleware.CsrfMiddleware'`` to your -list of middleware classes, :setting:`MIDDLEWARE_CLASSES`. It needs to process -the response after the SessionMiddleware, so must come before it in the list. It -also must process the response before things like compression or setting of -ETags happen to the response, so it must come after GZipMiddleware, -CommonMiddleware and ConditionalGetMiddleware in the list. +.. versionchanged:: 1.2 + The template tag functionality (the recommended way to use this) was added + in version 1.2. The previous method (still available) is described under + `Legacy method`_. -The ``CsrfMiddleware`` class is actually composed of two middleware: -``CsrfViewMiddleware`` which performs the checks on incoming requests, -and ``CsrfResponseMiddleware`` which performs post-processing of the -result. This allows the individual components to be used and/or -replaced instead of using ``CsrfMiddleware``. +To enable CSRF protection for your views, follow these steps: -.. versionchanged:: 1.1 - (previous versions of Django did not provide these two components - of ``CsrfMiddleware`` as described above) + 1. Add the middleware + ``'django.contrib.csrf.middleware.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 + 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. + + 2. In any template that uses a POST form, use the ``csrf_token`` tag inside + the ```` element if the form is for an internal URL, e.g.:: + + {% csrf_token %} + + This should not be done for POST forms that target external URLs, since + 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 + 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 + TEMPLATE_CONTEXT_PROCESSORS setting). If you are using + generic views or contrib apps, you are covered already, since these + apps use RequestContext throughout. + + 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.shortcuts import render_to_response + + def my_view(request): + c = {} + c.update(csrf(request)) + # ... view code here + return render_to_response("a_template.html", c) + + You may want to write your own ``render_to_response`` wrapper that + takes care of this step for you. + +The utility script ``extras/csrf_migration_helper.py`` can help to automate the +finding of code and templates that may need to be upgraded. It contains full +help on how to use it. + +Legacy method +------------- + +In Django 1.1, the template tag did not exist. Instead, a post-processing +middleware that re-wrote POST forms to include the CRSF token was used. If you +are upgrading a site from version 1.1 or earlier, please read this section and +the `Upgrading notes`_ below. The post-processing middleware is still available +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 + :setting:`MIDDLEWARE_CLASSES` setting. + + ``CsrfResponseMiddleware`` needs to process the response before things + like compression or setting ofETags happen to the response, so it must + come after ``GZipMiddleware``, ``CommonMiddleware`` and + ``ConditionalGetMiddleware`` in the list. It also must come after + ``CsrfViewMiddleware``. + +Use of the ``CsrfResponseMiddleware`` is not recommended because of the +performance hit it imposes, and because of a potential security problem (see +below). It can be used as an interim measure until applications have been +updated to use the ``{% crsf_token %}`` tag. It is deprecated and will be +removed in Django 1.4. + +Django 1.1 and earlier provided a single ``CsrfMiddleware`` class. This is also +still available for backwards compatibility. It combines the functions of the +two middleware. + +Note also that previous versions of these classes depended on the sessions +framework, but this dependency has now been removed, with backward compatibility +support so that upgrading will not produce any issues. + +Security of legacy method +~~~~~~~~~~~~~~~~~~~~~~~~~ + +The post-processing ``CsrfResponseMiddleware`` adds the CSRF token to all POST +forms (unless the view has been decorated with ``csrf_response_exempt``). If +the POST form has an external untrusted site as its target, rather than an +internal page, that site will be sent the CSRF token when the form is submitted. +Armed with this leaked information, that site will then be able to successfully +launch a CSRF attack on your site against that user. The +``@csrf_response_exempt`` decorator can be used to fix this, but only if the +page doesn't also contain internal forms that require the token. + +Upgrading notes +--------------- + +When upgrading to version 1.2 or later, you may have applications that rely on +the old post-processing functionality for CSRF protection, or you may not have +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. + +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, +``CsrfViewMiddleware`` and ``CsrfResponseMiddleware`` (in that order). + +If you do not have any of the middleware in your :setting:`MIDDLEWARE_CLASSES`, +you will have a working installation but without any CSRF protection for your +views (just as you had before). It is strongly recommended to install +``CsrfViewMiddleware`` and ``CsrfResponseMiddleware``, as described above. + +(Note that contrib apps, such as the admin, have been updated to use the +``csrf_protect`` decorator, so that they are secured even if you do not add the +``CsrfViewMiddleware`` to your settings). + +Assuming you have followed the above, all views in your Django site will now be +protected by the ``CsrfViewMiddleware``. Contrib apps meet the requirements +imposed by the ``CsrfViewMiddleware`` using the template tag, and other +applications in your project will meet its requirements by virtue of the +``CsrfResponseMiddleware``. + +The next step is to update all your applications to use the template tag, as +described in `How to use it`_, steps 2-3. This can be done as soon as is +practical. Any applications that are updated will now require Django 1.2 or +later, since they will use the CSRF template tag which was not available in +earlier versions. + +The utility script ``extras/csrf_migration_helper.py`` can help to automate the +finding of code and templates that may need to be upgraded. It contains full +help on how to use it. + +Finally, once all applications are upgraded, ``CsrfResponseMiddleware`` can be +removed from your settings. + +While ``CsrfResponseMiddleware`` is still in use, the ``csrf_response_exempt`` +decorator, described in `Exceptions`_, may be useful. The post-processing +middleware imposes a performance hit and a potential vulnerability, and any +views that have been upgraded to use the new template tag method no longer need +it. Exceptions ---------- .. versionadded:: 1.1 -To manually exclude a view function from being handled by the -CsrfMiddleware, you can use the ``csrf_exempt`` decorator, found in -the ``django.contrib.csrf.middleware`` module. For example:: +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:: from django.contrib.csrf.middleware import csrf_exempt @@ -54,71 +194,172 @@ the ``django.contrib.csrf.middleware`` module. For example:: return HttpResponse('Hello world') my_view = csrf_exempt(my_view) -Like the middleware itself, the ``csrf_exempt`` decorator is composed -of two parts: a ``csrf_view_exempt`` decorator and a -``csrf_response_exempt`` decorator, found in the same module. These -disable the view protection mechanism (``CsrfViewMiddleware``) and the -response post-processing (``CsrfResponseMiddleware``) respectively. -They can be used individually if required. +Like the middleware, the ``csrf_exempt`` decorator is composed of two parts: a +``csrf_view_exempt`` decorator and a ``csrf_response_exempt`` decorator, found +in the same module. These disable the view protection mechanism +(``CsrfViewMiddleware``) and the response post-processing +(``CsrfResponseMiddleware``) respectively. They can be used individually if +required. -You don't have to worry about doing this for most AJAX views. Any -request sent with "X-Requested-With: XMLHttpRequest" is automatically -exempt. (See the next section.) +You don't have to worry about doing this for most AJAX views. Any request sent +with "X-Requested-With: XMLHttpRequest" is automatically exempt. (See the `How +it works`_ section.) + +Subdomains +---------- + +By default, CSRF cookies are specific to the subdomain they are set for. This +means that a form served from one subdomain (e.g. server1.example.com) will not +be able to have a target on another subdomain (e.g. server2.example.com). This +restriction can be removed by setting :setting:`CSRF_COOKIE_DOMAIN` to be +something like ``".example.com"``. + +Please note that, with or without use of this setting, this CSRF protection +mechanism is not safe against cross-subdomain attacks -- see `Limitations`_. + +Rejected requests +================= + +By default, a '403 Forbidden' response is sent to the user if an incoming +request fails the checks performed by ``CsrfViewMiddleware``. This should +usually only be seen when there is a genuine Cross Site Request Forgery, or +when, due to a programming error, the CSRF token has not been included with a +POST form. + +No logging is done, and the error message is not very friendly, so you may want +to provide your own page for handling this condition. To do this, simply set +the :setting:`CSRF_FAILURE_VIEW` setting to a dotted path to your own view +function, which should have the following signature:: + + def csrf_failure(request, reason="") + +where ``reason`` is a short message (intended for developers or logging, not for +end users) indicating the reason the request was rejected. How it works ============ -CsrfMiddleware does two things: +The CSRF protection is based on the following things: -1. It modifies outgoing requests by adding a hidden form field to all - 'POST' forms, with the name 'csrfmiddlewaretoken' and a value which is - a hash of the session ID plus a secret. If there is no session ID set, - this modification of the response isn't done, so there is very little - performance penalty for those requests that don't have a session. - (This is done by ``CsrfResponseMiddleware``). +1. A CSRF cookie that is set to a random value (a session independent nonce, as + it is called), which other sites will not have access to. -2. On all incoming POST requests that have the session cookie set, it - checks that the 'csrfmiddlewaretoken' is present and correct. If it - isn't, the user will get a 403 error. (This is done by - ``CsrfViewMiddleware``) + 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()`` + (the function used internally to retrieve the CSRF token). -This ensures that only forms that have originated from your Web site -can be used to POST data back. +2. A hidden form field with the name 'csrfmiddlewaretoken' present in all + outgoing POST forms. The value of this field is the value of the CSRF + cookie. + + This part is done by the template tag (and with the legacy method, it is done + by ``CsrfResponseMiddleware``). + +3. For all incoming POST requests, a CSRF cookie must be present, and the + 'csrfmiddlewaretoken' field must be present and correct. If it isn't, the + user will get a 403 error. + + This check is done by ``CsrfViewMiddleware``. + +4. In addition, for HTTPS requests, strict referer checking is done by + ``CsrfViewMiddleware``. This is necessary to address a Man-In-The-Middle + attack that is possible under HTTPS when using a session independent nonce, + due to the fact that HTTP 'Set-Cookie' headers are (unfortunately) accepted + by clients that are talking to a site under HTTPS. (Referer checking is not + done for HTTP requests because the presence of the Referer header is not + reliable enough under HTTP.) + +This ensures that only forms that have originated from your Web site can be used +to POST data back. It deliberately only targets HTTP POST requests (and the corresponding POST -forms). GET requests ought never to have any potentially dangerous side -effects (see `9.1.1 Safe Methods, HTTP 1.1, RFC 2616`_), and so a -CSRF attack with a GET request ought to be harmless. +forms). GET requests ought never to have any potentially dangerous side effects +(see `9.1.1 Safe Methods, HTTP 1.1, RFC 2616`_), and so a CSRF attack with a GET +request ought to be harmless. -POST requests that are not accompanied by a session cookie are not protected, -but they do not need to be protected, since the 'attacking' Web site -could make these kind of requests anyway. +``CsrfResponseMiddleware`` checks the Content-Type before modifying the +response, and only pages that are served as 'text/html' or +'application/xml+xhtml' are modified. -The Content-Type is checked before modifying the response, and only -pages that are served as 'text/html' or 'application/xml+xhtml' -are modified. +AJAX +---- -The middleware tries to be smart about requests that come in via AJAX. Many -JavaScript toolkits send an "X-Requested-With: XMLHttpRequest" HTTP header; -these requests are detected and automatically *not* handled by this middleware. -We can do this safely because, in the context of a browser, the header can only -be added by using ``XMLHttpRequest``, and browsers already implement a -same-domain policy for ``XMLHttpRequest``. (Note that this is not secure if you -don't trust content within the same domain or subdomains.) +The middleware tries to be smart about requests that come in via AJAX. Most +modern JavaScript toolkits send an "X-Requested-With: XMLHttpRequest" HTTP +header; these requests are detected and automatically *not* handled by this +middleware. We can do this safely because, in the context of a browser, the +header can only be added by using ``XMLHttpRequest``, and browsers already +implement a same-domain policy for ``XMLHttpRequest``. +For the more recent browsers that relax this same-domain policy, custom headers +like "X-Requested-With" are only allowed after the browser has done a +'preflight' check to the server to see if the cross-domain request is allowed, +using a strictly 'opt in' mechanism, so the exception for AJAX is still safeā€”if +the developer has specifically opted in to allowing cross-site AJAX POST +requests on a specific URL, they obviously don't want the middleware to disallow +exactly that. .. _9.1.1 Safe Methods, HTTP 1.1, RFC 2616: http://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html +Caching +======= + +If the ``csrf_token`` template tag is used by a template (or the ``get_token`` +function is called some other way), ``CsrfViewMiddleware`` will add a cookie and +a ``Vary: Cookie`` header to the response. Similarly, +``CsrfResponseMiddleware`` will send the ``Vary: Cookie`` header if it inserted +a token. This means that these middleware will play well with the cache +middleware if it is used as instructed (``UpdateCacheMiddleware`` goes before +all other middleware). + +However, if you use cache decorators on individual views, the CSRF middleware +will not yet have been able to set the Vary header. In this case, on any views +that will require a CSRF token to be inserted you should use the +:func:`django.views.decorators.vary.vary_on_cookie` decorator first:: + + from django.views.decorators.cache import cache_page + from django.views.decorators.vary import vary_on_cookie + + @cache_page(60 * 15) + @vary_on_cookie + def my_view(request): + # ... + + +Testing +======= + +The ``CsrfViewMiddleware`` will usually be a big hindrance to testing view +functions, due to the need for the CSRF token which must be sent with every POST +request. For this reason, Django's HTTP client for tests has been modified to +set a flag on requests which relaxes the middleware and the ``csrf_protect`` +decorator so that they no longer rejects requests. In every other respect +(e.g. sending cookies etc.), they behave the same. + Limitations =========== -CsrfMiddleware requires Django's session framework to work. If you have -a custom authentication system that manually sets cookies and the like, -it won't help you. +Subdomains within a site will be able to set cookies on the client for the whole +domain. By setting the cookie and using a corresponding token, subdomains will +be able to circumvent the CSRF protection. The only way to avoid this is to +ensure that subdomains are controlled by trusted users (or, are at least unable +to set cookies). Note that even without CSRF, there are other vulnerabilities, +such as session fixation, that make giving subdomains to untrusted parties a bad +idea, and these vulnerabilities cannot easily be fixed with current browsers. -If 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. It may still be possible -to use the middleware, provided you can find some way to get the -CSRF token and ensure that is included when your form is submitted. +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 +the CSRF token and ensure it is included when your form is submitted. + +Contrib and reusable apps +========================= + +Because it is possible for the developer to turn off the ``CsrfViewMiddleware``, +all relevant views in contrib apps use the ``csrf_protect`` decorator to ensure +the security of these applications against CSRF. It is recommended that the +developers of other reusable apps that want the same guarantees also use the +``csrf_protect`` decorator on their views. diff --git a/docs/ref/contrib/formtools/form-wizard.txt b/docs/ref/contrib/formtools/form-wizard.txt index 98f0dbad42..11a2aed091 100644 --- a/docs/ref/contrib/formtools/form-wizard.txt +++ b/docs/ref/contrib/formtools/form-wizard.txt @@ -177,7 +177,7 @@ Here's a full example template: {% block content %}

Step {{ step }} of {{ step_count }}

- + {% csrf_token %} {{ form }}
diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index e8c673d995..92c1012292 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -144,6 +144,44 @@ Default: ``600`` The default number of seconds to cache a page when the caching middleware or ``cache_page()`` decorator is used. +.. setting:: CSRF_COOKIE_NAME + +CSRF_COOKIE_NAME +---------------- +Default: ``'csrftoken'`` + +The name of the cookie to use for the CSRF authentication token. This can be whatever you +want. See :ref:`ref-contrib-csrf`. + +.. setting:: CSRF_COOKIE_DOMAIN + +CSRF_COOKIE_DOMAIN +------------------ + +Default: ``None`` + +The domain to be used when setting the CSRF cookie. This can be useful for +allowing cross-subdomain requests to be exluded from the normal cross site +request forgery protection. It should be set to a string such as +``".lawrence.com"`` to allow a POST request from a form on one subdomain to be +accepted by accepted by a view served from another subdomain. + +.. setting:: CSRF_FAILURE_VIEW + +CSRF_FAILURE_VIEW +----------------- + +Default: ``'django.contrib.csrf.views.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:: + + def csrf_failure(request, reason="") + +where ``reason`` is a short message (intended for developers or logging, not for +end users) indicating the reason the request was rejected. See +:ref:`ref-contrib-csrf`. + .. setting:: DATABASE_ENGINE DATABASE_ENGINE @@ -751,6 +789,7 @@ Default:: ('django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.csrf.middleware.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 e3260a96f8..6d91571228 100644 --- a/docs/ref/templates/api.txt +++ b/docs/ref/templates/api.txt @@ -313,6 +313,13 @@ and return a dictionary of items to be merged into the context. By default, "django.core.context_processors.i18n", "django.core.context_processors.media") +.. versionadded:: 1.2 + In addition to these, ``RequestContext`` always uses + ``'django.contrib.csrf.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. + Each processor is applied in order. That means, if one processor adds a variable to the context and a second processor adds a variable with the same name, the second will override the first. The default processors are explained @@ -404,6 +411,14 @@ 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 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionadded:: 1.2 + +This processor adds a token that is needed by the ``csrf_token`` template tag +for protection against :ref:`Cross Site Request Forgeries `. + django.core.context_processors.request ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/ref/templates/builtins.txt b/docs/ref/templates/builtins.txt index a2f8b9f8b3..7084dc0a78 100644 --- a/docs/ref/templates/builtins.txt +++ b/docs/ref/templates/builtins.txt @@ -53,6 +53,13 @@ Ignore everything between ``{% comment %}`` and ``{% endcomment %}`` .. templatetag:: cycle +csrf_token +~~~~~~~~~~ + +.. versionadded:: 1.2 + +This is described in the documentation for :ref:`Cross Site Request Forgeries `. + cycle ~~~~~ diff --git a/docs/releases/1.2-alpha.txt b/docs/releases/1.2-alpha.txt index 0df2103486..abc701de7d 100644 --- a/docs/releases/1.2-alpha.txt +++ b/docs/releases/1.2-alpha.txt @@ -2,6 +2,22 @@ Backwards-incompatible changes ============================== +CSRF Protection +--------------- + +There have been large changes to the way that CSRF protection works, detailed in +:ref:`the CSRF documentaton `. The following are the major +changes that developers must be aware of: + + * ``CsrfResponseMiddleware`` and ``CsrfMiddleware`` have been deprecated, and + will be removed completely in Django 1.4, in favour of a template tag that + should be inserted into forms. + + * ``CsrfViewMiddleware`` is included in :setting:`MIDDLEWARE_CLASSES` by + default. This turns on CSRF protection by default, so that views that accept + POST requests need to be written to work with the middleware. Instructions + on how to do this are found in the CSRF docs. + LazyObject ---------- diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index c16bd3d183..33461a0858 100644 --- a/docs/topics/auth.txt +++ b/docs/topics/auth.txt @@ -767,7 +767,7 @@ the following line to your URLconf::

Your username and password didn't match. Please try again.

{% endif %} - + {% csrf_token %} diff --git a/docs/topics/http/middleware.txt b/docs/topics/http/middleware.txt index 19facb8371..f7bb73d6bd 100644 --- a/docs/topics/http/middleware.txt +++ b/docs/topics/http/middleware.txt @@ -29,6 +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.contrib.auth.middleware.AuthenticationMiddleware', ) diff --git a/extras/csrf_migration_helper.py b/extras/csrf_migration_helper.py new file mode 100644 index 0000000000..bc352a1762 --- /dev/null +++ b/extras/csrf_migration_helper.py @@ -0,0 +1,369 @@ +#!/usr/bin/env python + +# This script aims to help developers locate forms and view code that needs to +# use the new CSRF protection in Django 1.2. It tries to find all the code that +# may need the steps described in the CSRF documentation. It does not modify +# any code directly, it merely attempts to locate it. Developers should be +# aware of its limitations, described below. +# +# For each template that contains at least one POST form, the following info is printed: +# +# +# AKA: +# POST forms: +# With token: +# Without token: +# +# +# Searching for: +#
{{ form.username.label_tag }}