Permalink
Browse files

Fixed #9977 - CsrfMiddleware gets template tag added, session depende…

…ncy 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
  • Loading branch information...
1 parent d1da261 commit 8e70cef9b67433edd70935dcc30c621d1e7fc0a0 @spookylukey spookylukey committed Oct 26, 2009
Showing with 1,427 additions and 234 deletions.
  1. +2 −0 AUTHORS
  2. +13 −0 django/conf/global_settings.py
  3. +1 −0 django/conf/project_template/settings.py
  4. +7 −2 django/contrib/admin/options.py
  5. +5 −0 django/contrib/admin/sites.py
  6. +1 −1 django/contrib/admin/templates/admin/auth/user/change_password.html
  7. +1 −1 django/contrib/admin/templates/admin/change_form.html
  8. +1 −1 django/contrib/admin/templates/admin/change_list.html
  9. +1 −1 django/contrib/admin/templates/admin/delete_confirmation.html
  10. +1 −1 django/contrib/admin/templates/admin/delete_selected_confirmation.html
  11. +1 −1 django/contrib/admin/templates/admin/login.html
  12. +1 −1 django/contrib/admin/templates/admin/template_validator.html
  13. +1 −1 django/contrib/admin/templates/registration/password_change_form.html
  14. +1 −1 django/contrib/admin/templates/registration/password_reset_confirm.html
  15. +1 −1 django/contrib/admin/templates/registration/password_reset_form.html
  16. +7 −2 django/contrib/auth/views.py
  17. +1 −1 django/contrib/comments/templates/comments/approve.html
  18. +1 −1 django/contrib/comments/templates/comments/delete.html
  19. +1 −1 django/contrib/comments/templates/comments/flag.html
  20. +1 −1 django/contrib/comments/templates/comments/form.html
  21. +1 −1 django/contrib/comments/templates/comments/preview.html
  22. +3 −2 django/contrib/comments/views/comments.py
  23. +4 −0 django/contrib/comments/views/moderation.py
  24. +20 −0 django/contrib/csrf/context_processors.py
  25. +10 −0 django/contrib/csrf/decorators.py
  26. +192 −64 django/contrib/csrf/middleware.py
  27. +245 −66 django/contrib/csrf/tests.py
  28. +62 −0 django/contrib/csrf/views.py
  29. +1 −1 django/contrib/formtools/templates/formtools/form.html
  30. +2 −2 django/contrib/formtools/templates/formtools/preview.html
  31. +6 −3 django/contrib/formtools/tests.py
  32. +2 −0 django/contrib/formtools/wizard.py
  33. +9 −1 django/template/context.py
  34. +21 −0 django/template/defaulttags.py
  35. +5 −0 django/test/client.py
  36. +6 −0 docs/internals/deprecation.txt
  37. +25 −1 docs/intro/tutorial04.txt
  38. +312 −71 docs/ref/contrib/csrf.txt
  39. +1 −1 docs/ref/contrib/formtools/form-wizard.txt
  40. +39 −0 docs/ref/settings.txt
  41. +15 −0 docs/ref/templates/api.txt
  42. +7 −0 docs/ref/templates/builtins.txt
  43. +16 −0 docs/releases/1.2-alpha.txt
  44. +1 −1 docs/topics/auth.txt
  45. +1 −0 docs/topics/http/middleware.txt
  46. +369 −0 extras/csrf_migration_helper.py
  47. +3 −2 tests/regressiontests/admin_views/tests.py
View
@@ -470,6 +470,8 @@ answer newbie questions, and generally made Django that much better:
Gasper Zejn <zejn@kiberpipa.org>
Jarek Zgoda <jarek.zgoda@gmail.com>
Cheng Zhang
+ Glenn
+ bthomas
A big THANK YOU goes to:
@@ -300,6 +300,7 @@
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 @@
# 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 #
###########
@@ -60,6 +60,7 @@
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.contrib.csrf.middleware.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
)
@@ -6,6 +6,7 @@
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 @@ def response_action(self, request, queryset):
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 @@ def add_view(self, request, form_url='', extra_context=None):
}
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 @@ def change_view(self, request, object_id, extra_context=None):
}
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 @@ def changelist_view(self, request, extra_context=None):
'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
@@ -3,6 +3,8 @@
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 @@ def inner(request, *args, **kwargs):
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):
@@ -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 %}
@@ -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 %}
@@ -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 %}
@@ -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" %}" />
@@ -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 }}" />
@@ -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>
@@ -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>
@@ -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>
@@ -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 }}
@@ -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>
@@ -4,6 +4,7 @@
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.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))
@@ -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>
@@ -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>
@@ -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>
@@ -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 %}
@@ -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>
@@ -10,6 +10,7 @@
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 @@ def __init__(self, why):
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."""
@@ -5,7 +5,9 @@
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):
"""
@@ -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() }
@@ -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.
+"""
Oops, something went wrong.

0 comments on commit 8e70cef

Please sign in to comment.