Skip to content

Commit

Permalink
Fixed #9977 - CsrfMiddleware gets template tag added, session depende…
Browse files Browse the repository at this point in the history
…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
spookylukey committed Oct 26, 2009
1 parent d1da261 commit 8e70cef
Show file tree
Hide file tree
Showing 47 changed files with 1,427 additions and 234 deletions.
2 changes: 2 additions & 0 deletions AUTHORS
Expand Up @@ -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:

Expand Down
13 changes: 13 additions & 0 deletions django/conf/global_settings.py
Expand Up @@ -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',
Expand Down Expand Up @@ -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 #
###########
Expand Down
1 change: 1 addition & 0 deletions django/conf/project_template/settings.py
Expand Up @@ -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',
)

Expand Down
9 changes: 7 additions & 2 deletions django/contrib/admin/options.py
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions django/contrib/admin/sites.py
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
Expand Up @@ -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 %}
Expand Down
2 changes: 1 addition & 1 deletion django/contrib/admin/templates/admin/change_form.html
Expand Up @@ -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 %}
Expand Down
2 changes: 1 addition & 1 deletion django/contrib/admin/templates/admin/change_list.html
Expand Up @@ -68,7 +68,7 @@ <h2>{% trans 'Filter' %}</h2>
{% 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 %}
Expand Down
Expand Up @@ -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" %}" />
Expand Down
Expand Up @@ -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 }}" />
Expand Down
2 changes: 1 addition & 1 deletion django/contrib/admin/templates/admin/login.html
Expand Up @@ -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>
Expand Down
Expand Up @@ -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>
Expand Down
Expand Up @@ -11,7 +11,7 @@ <h1>{% trans 'Password change' %}</h1>

<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>
Expand Down
Expand Up @@ -13,7 +13,7 @@ <h1>{% trans 'Enter new password' %}</h1>

<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 }}
Expand Down
Expand Up @@ -11,7 +11,7 @@ <h1>{% trans "Password reset" %}</h1>

<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>
Expand Down
9 changes: 7 additions & 2 deletions django/contrib/auth/views.py
Expand Up @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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."
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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:
Expand All @@ -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))
2 changes: 1 addition & 1 deletion django/contrib/comments/templates/comments/approve.html
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion django/contrib/comments/templates/comments/delete.html
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion django/contrib/comments/templates/comments/flag.html
Expand Up @@ -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>
Expand Down
2 changes: 1 addition & 1 deletion 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 %}
Expand Down
2 changes: 1 addition & 1 deletion django/contrib/comments/templates/comments/preview.html
Expand Up @@ -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>
Expand Down
5 changes: 3 additions & 2 deletions django/contrib/comments/views/comments.py
Expand Up @@ -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):
"""
Expand All @@ -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.
Expand Down Expand Up @@ -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."""
Expand Down
4 changes: 4 additions & 0 deletions django/contrib/comments/views/moderation.py
Expand Up @@ -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):
"""
Expand All @@ -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):
"""
Expand All @@ -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):
"""
Expand Down
20 changes: 20 additions & 0 deletions 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() }
10 changes: 10 additions & 0 deletions 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.
"""

0 comments on commit 8e70cef

Please sign in to comment.