diff options
| author | Luke Plant <L.Plant.98@cantab.net> | 2009-10-26 23:23:07 +0000 |
|---|---|---|
| committer | Luke Plant <L.Plant.98@cantab.net> | 2009-10-26 23:23:07 +0000 |
| commit | 8e70cef9b67433edd70935dcc30c621d1e7fc0a0 (patch) | |
| tree | 9dc32d96165c27bb0be761cce3de5c85e0ccf9a5 /django | |
| parent | d1da26141788f8b359d96c49bc596125598d23ee (diff) | |
Fixed #9977 - CsrfMiddleware gets template tag added, session dependency removed, and turned on by default.
This is a large change to CSRF protection for Django. It includes:
* removing the dependency on the session framework.
* deprecating CsrfResponseMiddleware, and replacing with a core template tag.
* turning on CSRF protection by default by adding CsrfViewMiddleware to
the default value of MIDDLEWARE_CLASSES.
* protecting all contrib apps (whatever is in settings.py)
using a decorator.
For existing users of the CSRF functionality, it should be a seamless update,
but please note that it includes DEPRECATION of features in Django 1.1,
and there are upgrade steps which are detailed in the docs.
Many thanks to 'Glenn' and 'bthomas', who did a lot of the thinking and work
on the patch, and to lots of other people including Simon Willison and
Russell Keith-Magee who refined the ideas.
Details of the rationale for these changes is found here:
http://code.djangoproject.com/wiki/CsrfProtection
As of this commit, the CSRF code is mainly in 'contrib'. The code will be
moved to core in a separate commit, to make the changeset as readable as
possible.
git-svn-id: http://code.djangoproject.com/svn/django/trunk@11660 bcc190cf-cafb-0310-a4f2-bffc1f526a37
Diffstat (limited to 'django')
34 files changed, 630 insertions, 158 deletions
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 @@ </div> {% endif %}{% endblock %} {% block content %}<div id="content-main"> -<form action="{{ form_url }}" method="post" id="{{ opts.module_name }}_form">{% block form_top %}{% endblock %} +<form action="{{ form_url }}" method="post" id="{{ opts.module_name }}_form">{% csrf_token %}{% block form_top %}{% endblock %} <div> {% if is_popup %}<input type="hidden" name="_popup" value="1" />{% 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 @@ </ul> {% endif %}{% endif %} {% endblock %} -<form {% if has_file_field %}enctype="multipart/form-data" {% endif %}action="{{ form_url }}" method="post" id="{{ opts.module_name }}_form">{% block form_top %}{% endblock %} +<form {% if has_file_field %}enctype="multipart/form-data" {% endif %}action="{{ form_url }}" method="post" id="{{ opts.module_name }}_form">{% csrf_token %}{% block form_top %}{% endblock %} <div> {% if is_popup %}<input type="hidden" name="_popup" value="1" />{% 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 %} - <form action="" method="post"{% if cl.formset.is_multipart %} enctype="multipart/form-data"{% endif %}> + <form action="" method="post"{% if cl.formset.is_multipart %} enctype="multipart/form-data"{% endif %}>{% 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 %} <p>{% 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 %}</p> <ul>{{ deleted_objects|unordered_list }}</ul> - <form action="" method="post"> + <form action="" method="post">{% csrf_token %} <div> <input type="hidden" name="post" value="yes" /> <input type="submit" value="{% trans "Yes, I'm sure" %}" /> 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 %} <ul>{{ deleteable_object|unordered_list }}</ul> {% endfor %} - <form action="" method="post"> + <form action="" method="post">{% csrf_token %} <div> {% for obj in queryset %} <input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk }}" /> 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 @@ <p class="errornote">{{ error_message }}</p> {% endif %} <div id="content-main"> -<form action="{{ app_path }}" method="post" id="login-form"> +<form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %} <div class="form-row"> <label for="id_username">{% trans 'Username:' %}</label> <input type="text" name="username" id="id_username" /> </div> 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 @@ <div id="content-main"> -<form action="" method="post"> +<form action="" method="post">{% csrf_token %} {% if form.errors %} <p class="errornote">Your template had {{ form.errors|length }} error{{ form.errors|pluralize }}:</p> 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 @@ <p>{% 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." %}</p> -<form action="" method="post"> +<form action="" method="post">{% csrf_token %} {{ form.old_password.errors }} <p class="aligned wide"><label for="id_old_password">{% trans 'Old password:' %}</label>{{ form.old_password }}</p> 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 @@ <p>{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}</p> -<form action="" method="post"> +<form action="" method="post">{% csrf_token %} {{ form.new_password1.errors }} <p class="aligned wide"><label for="id_new_password1">{% trans 'New password:' %}</label>{{ form.new_password1 }}</p> {{ 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 @@ <p>{% trans "Forgotten your password? Enter your e-mail address below, and we'll e-mail instructions for setting a new one." %}</p> -<form action="" method="post"> +<form action="" method="post">{% csrf_token %} {{ form.email.errors }} <p><label for="id_email">{% trans 'E-mail address:' %}</label> {{ form.email }} <input type="submit" value="{% trans 'Reset my password' %}" /></p> </form> 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 %} <h1>{% trans "Really make this comment public?" %}</h1> <blockquote>{{ comment|linebreaks }}</blockquote> - <form action="." method="post"> + <form action="." method="post">{% csrf_token %} {% if next %}<input type="hidden" name="next" value="{{ next }}" id="next" />{% endif %} <p class="submit"> <input type="submit" name="submit" value="{% trans "Approve" %}" /> or <a href="{{ comment.get_absolute_url }}">cancel</a> 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 %} <h1>{% trans "Really remove this comment?" %}</h1> <blockquote>{{ comment|linebreaks }}</blockquote> - <form action="." method="post"> + <form action="." method="post">{% csrf_token %} {% if next %}<input type="hidden" name="next" value="{{ next }}" id="next" />{% endif %} <p class="submit"> <input type="submit" name="submit" value="{% trans "Remove" %}" /> or <a href="{{ comment.get_absolute_url }}">cancel</a> 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 %} <h1>{% trans "Really flag this comment?" %}</h1> <blockquote>{{ comment|linebreaks }}</blockquote> - <form action="." method="post"> + <form action="." method="post">{% csrf_token %} {% if next %}<input type="hidden" name="next" value="{{ next }}" id="next" />{% endif %} <p class="submit"> <input type="submit" name="submit" value="{% trans "Flag" %}" /> or <a href="{{ comment.get_absolute_url }}">cancel</a> 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 %} -<form action="{% comment_form_target %}" method="post"> +<form action="{% comment_form_target %}" method="post">{% csrf_token %} {% if next %}<input type="hidden" name="next" value="{{ 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 %} - <form action="{% comment_form_target %}" method="post"> + <form action="{% comment_form_target %}" method="post">{% csrf_token %} {% if next %}<input type="hidden" name="next" value="{{ next }}" />{% endif %} {% if form.errors %} <h1>{% blocktrans count form.errors|length as counter %}Please correct the error below{% plural %}Please correct the errors below{% endblocktrans %}</h1> 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('<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"><body><h1>403 Forbidden</h1><p>Cross Site Request Forgery detected. Request aborted.</p></body></html>') - _POST_FORM_RE = \ re.compile(r'(<form\W[^>]*\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.") - csrf_token = _make_token(session_id) - # check incoming token - try: - request_csrf_token = request.POST['csrfmiddlewaretoken'] - except KeyError: - return HttpResponseForbidden(_ERROR_MSG) + # 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 HttpResponseForbidden(_ERROR_MSG) + return reject("CSRF token missing or incorrect.") + + return accept() + + def process_response(self, request, response): + if getattr(response, 'csrf_processing_done', False): + return response - return None + # 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 <form> tag plus the added <input> element""" return mark_safe(match.group() + "<div style='display:none;'>" + \ @@ -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. + 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() - If you are setting cookies directly, instead of using Django's - session framework, this middleware will not work. + 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) - CsrfMiddleWare is composed of two middleware, CsrfViewMiddleware - and CsrfResponseMiddleware which can be used independently. - """ - pass + 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=""" <html><body><form method="POST"><input type="text" /></form></body></html> """, 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_session_request_with_token(self): - req = self._get_POST_session_request() - req.POST['csrfmiddlewaretoken'] = _make_token(self._session_id) + 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_form_response(self): - return post_form_response() - - def _get_new_session_response(self): - resp = self._get_post_form_response() - resp.cookies[settings.SESSION_COOKIE_NAME] = self._session_id - return resp + 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 _check_token_present(self, response): - self.assertContains(response, "name='csrfmiddlewaretoken' value='%s'" % _make_token(self._session_id)) + 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_view(self): - return test_view + 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 - 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) - self.assertEquals(resp_content, resp2.content) - def test_process_response_existing_session(self): + 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 the token is inserted if there is an existing session + Check that if get_token() is not called, the view middleware does not + add a cookie. """ - req = self._get_GET_session_request() - resp = self._get_post_form_response() + # 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_new_session(self): + def test_process_response_non_html(self): """ - Check that the token is inserted if there is a new session being started + Check the the post-processor does nothing for content-types not in _HTML_TYPES. """ - req = self._get_GET_no_session_request() # no session in request - resp = self._get_new_session_response() # but new session started + 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.assertNotEqual(resp_content, resp2.content) - self._check_token_present(resp2) + 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_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(), (), {}) - self.assertEquals(None, req2) + 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_session_no_token(self): + def test_process_request_csrf_cookie_no_token(self): """ - Check that if a session is present but no token, we get a 'forbidden' + Check that if a CSRF cookie is present but no token, the middleware + rejects the incoming request. """ - req = self._get_POST_session_request() - req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {}) - self.assertEquals(HttpResponseForbidden, req2.__class__) + 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_session_and_token(self): + def test_process_request_csrf_cookie_and_token(self): """ - Check that if a session is present and a token, the middleware lets it through + Check that if both a cookie and a token is present, the middleware lets it through. """ - req = self._get_POST_session_request_with_token() - req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {}) + 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_exempt_view(self): + 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. """ - Check that if a session is present and no token, but the csrf_exempt + 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_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 = """ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> +<html lang="en"> +<head> + <meta http-equiv="content-type" content="text/html; charset=utf-8"> + <title>403 Forbidden</title> +</head> +<body> + <h1>403 Forbidden</h1> + <p>CSRF verification failed. Request aborted.</p> + {% if DEBUG %} + <h2>Help</h2> + {% if reason %} + <p>Reason given for failure:</p> + <pre> + {{ reason }} + </pre> + {% endif %} + + <p>In general, this can occur when there is a genuine Cross Site Request Forgery, or when + <a + href='http://docs.djangoproject.com/en/dev/ref/contrib/csrf/#ref-contrib-csrf'>Django's + CSRF mechanism</a> has not been used correctly. For POST forms, you need to + ensure:</p> + + <ul> + <li>The view function uses <a + href='http://docs.djangoproject.com/en/dev/ref/templates/api/#subclassing-context-requestcontext'><tt>RequestContext</tt></a> + for the template, instead of <tt>Context</tt>.</li> + + <li>In the template, there is a <tt>{% templatetag openblock %} csrf_token + {% templatetag closeblock %}</tt> template tag inside each POST form that + targets an internal URL.</li> + </ul> + + <p>You're seeing the help section of this page because you have <code>DEBUG = + True</code> in your Django settings file. Change that to <code>False</code>, + and only the initial error message will be displayed. </p> + + <p>You can customize this page using the CSRF_FAILURE_VIEW setting.</p> + + {% endif %} +</body> +</html> +""" + +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 %}<h1>Please correct the following errors</h1>{% else %}<h1>Submit</h1>{% endif %} -<form action="" method="post"> +<form action="" method="post">{% csrf_token %} <table> {{ form }} </table> 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 @@ <p>Security hash: {{ hash_value }}</p> -<form action="" method="post"> +<form action="" method="post">{% csrf_token %} {% for field in form %}{{ field.as_hidden }} {% endfor %} <input type="hidden" name="{{ stage_field }}" value="2" /> @@ -25,7 +25,7 @@ <h1>Or edit it again</h1> -<form action="" method="post"> +<form action="" method="post">{% csrf_token %} <table> {{ form }} </table> 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"<div style='display:none'><input type='hidden' name='csrfmiddlewaretoken' value='%s' /></div>" % (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. |
