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
This commit is contained in:
Luke Plant 2009-10-26 23:23:07 +00:00
parent d1da261417
commit 8e70cef9b6
47 changed files with 1439 additions and 246 deletions

View File

@ -470,6 +470,8 @@ answer newbie questions, and generally made Django that much better:
Gasper Zejn <zejn@kiberpipa.org> Gasper Zejn <zejn@kiberpipa.org>
Jarek Zgoda <jarek.zgoda@gmail.com> Jarek Zgoda <jarek.zgoda@gmail.com>
Cheng Zhang Cheng Zhang
Glenn
bthomas
A big THANK YOU goes to: A big THANK YOU goes to:

View File

@ -300,6 +300,7 @@ DEFAULT_INDEX_TABLESPACE = ''
MIDDLEWARE_CLASSES = ( MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.csrf.middleware.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
# 'django.middleware.http.ConditionalGetMiddleware', # 'django.middleware.http.ConditionalGetMiddleware',
# 'django.middleware.gzip.GZipMiddleware', # 'django.middleware.gzip.GZipMiddleware',
@ -374,6 +375,18 @@ LOGIN_REDIRECT_URL = '/accounts/profile/'
# The number of days a password reset link is valid for # The number of days a password reset link is valid for
PASSWORD_RESET_TIMEOUT_DAYS = 3 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 # # TESTING #
########### ###########

View File

@ -60,6 +60,7 @@ TEMPLATE_LOADERS = (
MIDDLEWARE_CLASSES = ( MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.csrf.middleware.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
) )

View File

@ -6,6 +6,7 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.admin import widgets from django.contrib.admin import widgets
from django.contrib.admin import helpers 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.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.core.exceptions import PermissionDenied
from django.db import models, transaction from django.db import models, transaction
from django.db.models.fields import BLANK_CHOICE_DASH from django.db.models.fields import BLANK_CHOICE_DASH
@ -701,6 +702,8 @@ class ModelAdmin(BaseModelAdmin):
else: else:
return HttpResponseRedirect(".") return HttpResponseRedirect(".")
@csrf_protect
@transaction.commit_on_success
def add_view(self, request, form_url='', extra_context=None): def add_view(self, request, form_url='', extra_context=None):
"The 'add' admin view for this model." "The 'add' admin view for this model."
model = self.model model = self.model
@ -782,8 +785,9 @@ class ModelAdmin(BaseModelAdmin):
} }
context.update(extra_context or {}) context.update(extra_context or {})
return self.render_change_form(request, context, form_url=form_url, add=True) 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): def change_view(self, request, object_id, extra_context=None):
"The 'change' admin view for this model." "The 'change' admin view for this model."
model = self.model model = self.model
@ -871,8 +875,8 @@ class ModelAdmin(BaseModelAdmin):
} }
context.update(extra_context or {}) context.update(extra_context or {})
return self.render_change_form(request, context, change=True, obj=obj) 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): def changelist_view(self, request, extra_context=None):
"The 'change list' admin view for this model." "The 'change list' admin view for this model."
from django.contrib.admin.views.main import ChangeList, ERROR_FLAG from django.contrib.admin.views.main import ChangeList, ERROR_FLAG
@ -985,6 +989,7 @@ class ModelAdmin(BaseModelAdmin):
'admin/change_list.html' 'admin/change_list.html'
], context, context_instance=context_instance) ], context, context_instance=context_instance)
@csrf_protect
def delete_view(self, request, object_id, extra_context=None): def delete_view(self, request, object_id, extra_context=None):
"The 'delete' admin view for this model." "The 'delete' admin view for this model."
opts = self.model._meta opts = self.model._meta

View File

@ -3,6 +3,8 @@ from django import http, template
from django.contrib.admin import ModelAdmin from django.contrib.admin import ModelAdmin
from django.contrib.admin import actions from django.contrib.admin import actions
from django.contrib.auth import authenticate, login 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.db.models.base import ModelBase
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
@ -186,6 +188,9 @@ class AdminSite(object):
return view(request, *args, **kwargs) return view(request, *args, **kwargs)
if not cacheable: if not cacheable:
inner = never_cache(inner) 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) return update_wrapper(inner, view)
def get_urls(self): def get_urls(self):

View File

@ -15,7 +15,7 @@
</div> </div>
{% endif %}{% endblock %} {% endif %}{% endblock %}
{% block content %}<div id="content-main"> {% 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> <div>
{% if is_popup %}<input type="hidden" name="_popup" value="1" />{% endif %} {% if is_popup %}<input type="hidden" name="_popup" value="1" />{% endif %}
{% if form.errors %} {% if form.errors %}

View File

@ -29,7 +29,7 @@
</ul> </ul>
{% endif %}{% endif %} {% endif %}{% endif %}
{% endblock %} {% 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> <div>
{% if is_popup %}<input type="hidden" name="_popup" value="1" />{% endif %} {% if is_popup %}<input type="hidden" name="_popup" value="1" />{% endif %}
{% if save_on_top %}{% submit_row %}{% endif %} {% if save_on_top %}{% submit_row %}{% endif %}

View File

@ -68,7 +68,7 @@
{% endif %} {% endif %}
{% endblock %} {% 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 %} {% if cl.formset %}
{{ cl.formset.management_form }} {{ cl.formset.management_form }}
{% endif %} {% endif %}

View File

@ -22,7 +22,7 @@
{% else %} {% 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> <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> <ul>{{ deleted_objects|unordered_list }}</ul>
<form action="" method="post"> <form action="" method="post">{% csrf_token %}
<div> <div>
<input type="hidden" name="post" value="yes" /> <input type="hidden" name="post" value="yes" />
<input type="submit" value="{% trans "Yes, I'm sure" %}" /> <input type="submit" value="{% trans "Yes, I'm sure" %}" />

View File

@ -23,7 +23,7 @@
{% for deleteable_object in deletable_objects %} {% for deleteable_object in deletable_objects %}
<ul>{{ deleteable_object|unordered_list }}</ul> <ul>{{ deleteable_object|unordered_list }}</ul>
{% endfor %} {% endfor %}
<form action="" method="post"> <form action="" method="post">{% csrf_token %}
<div> <div>
{% for obj in queryset %} {% for obj in queryset %}
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk }}" /> <input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk }}" />

View File

@ -14,7 +14,7 @@
<p class="errornote">{{ error_message }}</p> <p class="errornote">{{ error_message }}</p>
{% endif %} {% endif %}
<div id="content-main"> <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"> <div class="form-row">
<label for="id_username">{% trans 'Username:' %}</label> <input type="text" name="username" id="id_username" /> <label for="id_username">{% trans 'Username:' %}</label> <input type="text" name="username" id="id_username" />
</div> </div>

View File

@ -4,7 +4,7 @@
<div id="content-main"> <div id="content-main">
<form action="" method="post"> <form action="" method="post">{% csrf_token %}
{% if form.errors %} {% if form.errors %}
<p class="errornote">Your template had {{ form.errors|length }} error{{ form.errors|pluralize }}:</p> <p class="errornote">Your template had {{ form.errors|length }} error{{ form.errors|pluralize }}:</p>

View File

@ -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> <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 }} {{ form.old_password.errors }}
<p class="aligned wide"><label for="id_old_password">{% trans 'Old password:' %}</label>{{ form.old_password }}</p> <p class="aligned wide"><label for="id_old_password">{% trans 'Old password:' %}</label>{{ form.old_password }}</p>

View File

@ -13,7 +13,7 @@
<p>{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}</p> <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 }} {{ form.new_password1.errors }}
<p class="aligned wide"><label for="id_new_password1">{% trans 'New password:' %}</label>{{ form.new_password1 }}</p> <p class="aligned wide"><label for="id_new_password1">{% trans 'New password:' %}</label>{{ form.new_password1 }}</p>
{{ form.new_password2.errors }} {{ form.new_password2.errors }}

View File

@ -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> <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 }} {{ form.email.errors }}
<p><label for="id_email">{% trans 'E-mail address:' %}</label> {{ form.email }} <input type="submit" value="{% trans 'Reset my password' %}" /></p> <p><label for="id_email">{% trans 'E-mail address:' %}</label> {{ form.email }} <input type="submit" value="{% trans 'Reset my password' %}" /></p>
</form> </form>

View File

@ -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 AuthenticationForm
from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm, PasswordChangeForm from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm, PasswordChangeForm
from django.contrib.auth.tokens import default_token_generator 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.core.urlresolvers import reverse
from django.shortcuts import render_to_response, get_object_or_404 from django.shortcuts import render_to_response, get_object_or_404
from django.contrib.sites.models import Site, RequestSite 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.contrib.auth.models import User
from django.views.decorators.cache import never_cache from django.views.decorators.cache import never_cache
@csrf_protect
@never_cache
def login(request, template_name='registration/login.html', def login(request, template_name='registration/login.html',
redirect_field_name=REDIRECT_FIELD_NAME, redirect_field_name=REDIRECT_FIELD_NAME,
authentication_form=AuthenticationForm): authentication_form=AuthenticationForm):
@ -43,7 +46,6 @@ def login(request, template_name='registration/login.html',
'site': current_site, 'site': current_site,
'site_name': current_site.name, 'site_name': current_site.name,
}, context_instance=RequestContext(request)) }, 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): 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." "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 # prompts for a new password
# - password_reset_complete shows a success message for the above # - 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', def password_reset(request, is_admin_site=False, template_name='registration/password_reset_form.html',
email_template_name='registration/password_reset_email.html', email_template_name='registration/password_reset_email.html',
password_reset_form=PasswordResetForm, token_generator=default_token_generator, 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'): def password_reset_done(request, template_name='registration/password_reset_done.html'):
return render_to_response(template_name, context_instance=RequestContext(request)) 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', 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, token_generator=default_token_generator, set_password_form=SetPasswordForm,
post_reset_redirect=None): 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, return render_to_response(template_name, context_instance=RequestContext(request,
{'login_url': settings.LOGIN_URL})) {'login_url': settings.LOGIN_URL}))
@csrf_protect
@login_required
def password_change(request, template_name='registration/password_change_form.html', def password_change(request, template_name='registration/password_change_form.html',
post_change_redirect=None, password_change_form=PasswordChangeForm): post_change_redirect=None, password_change_form=PasswordChangeForm):
if post_change_redirect is None: 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, { return render_to_response(template_name, {
'form': form, 'form': form,
}, context_instance=RequestContext(request)) }, context_instance=RequestContext(request))
password_change = login_required(password_change)
def password_change_done(request, template_name='registration/password_change_done.html'): def password_change_done(request, template_name='registration/password_change_done.html'):
return render_to_response(template_name, context_instance=RequestContext(request)) return render_to_response(template_name, context_instance=RequestContext(request))

View File

@ -6,7 +6,7 @@
{% block content %} {% block content %}
<h1>{% trans "Really make this comment public?" %}</h1> <h1>{% trans "Really make this comment public?" %}</h1>
<blockquote>{{ comment|linebreaks }}</blockquote> <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 %} {% if next %}<input type="hidden" name="next" value="{{ next }}" id="next" />{% endif %}
<p class="submit"> <p class="submit">
<input type="submit" name="submit" value="{% trans "Approve" %}" /> or <a href="{{ comment.get_absolute_url }}">cancel</a> <input type="submit" name="submit" value="{% trans "Approve" %}" /> or <a href="{{ comment.get_absolute_url }}">cancel</a>

View File

@ -6,7 +6,7 @@
{% block content %} {% block content %}
<h1>{% trans "Really remove this comment?" %}</h1> <h1>{% trans "Really remove this comment?" %}</h1>
<blockquote>{{ comment|linebreaks }}</blockquote> <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 %} {% if next %}<input type="hidden" name="next" value="{{ next }}" id="next" />{% endif %}
<p class="submit"> <p class="submit">
<input type="submit" name="submit" value="{% trans "Remove" %}" /> or <a href="{{ comment.get_absolute_url }}">cancel</a> <input type="submit" name="submit" value="{% trans "Remove" %}" /> or <a href="{{ comment.get_absolute_url }}">cancel</a>

View File

@ -6,7 +6,7 @@
{% block content %} {% block content %}
<h1>{% trans "Really flag this comment?" %}</h1> <h1>{% trans "Really flag this comment?" %}</h1>
<blockquote>{{ comment|linebreaks }}</blockquote> <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 %} {% if next %}<input type="hidden" name="next" value="{{ next }}" id="next" />{% endif %}
<p class="submit"> <p class="submit">
<input type="submit" name="submit" value="{% trans "Flag" %}" /> or <a href="{{ comment.get_absolute_url }}">cancel</a> <input type="submit" name="submit" value="{% trans "Flag" %}" /> or <a href="{{ comment.get_absolute_url }}">cancel</a>

View File

@ -1,5 +1,5 @@
{% load comments i18n %} {% 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 %} {% if next %}<input type="hidden" name="next" value="{{ next }}" />{% endif %}
{% for field in form %} {% for field in form %}
{% if field.is_hidden %} {% if field.is_hidden %}

View File

@ -5,7 +5,7 @@
{% block content %} {% block content %}
{% load comments %} {% 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 next %}<input type="hidden" name="next" value="{{ next }}" />{% endif %}
{% if form.errors %} {% if form.errors %}
<h1>{% blocktrans count form.errors|length as counter %}Please correct the error below{% plural %}Please correct the errors below{% endblocktrans %}</h1> <h1>{% blocktrans count form.errors|length as counter %}Please correct the error below{% plural %}Please correct the errors below{% endblocktrans %}</h1>

View File

@ -10,6 +10,7 @@ from django.utils.html import escape
from django.views.decorators.http import require_POST from django.views.decorators.http import require_POST
from django.contrib import comments from django.contrib import comments
from django.contrib.comments import signals from django.contrib.comments import signals
from django.contrib.csrf.decorators import csrf_protect
class CommentPostBadRequest(http.HttpResponseBadRequest): class CommentPostBadRequest(http.HttpResponseBadRequest):
""" """
@ -22,6 +23,8 @@ class CommentPostBadRequest(http.HttpResponseBadRequest):
if settings.DEBUG: if settings.DEBUG:
self.content = render_to_string("comments/400-debug.html", {"why": why}) self.content = render_to_string("comments/400-debug.html", {"why": why})
@csrf_protect
@require_POST
def post_comment(request, next=None): def post_comment(request, next=None):
""" """
Post a comment. 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()) return next_redirect(data, next, comment_done, c=comment._get_pk_val())
post_comment = require_POST(post_comment)
comment_done = confirmation_view( comment_done = confirmation_view(
template = "comments/posted.html", template = "comments/posted.html",
doc = """Display a "comment was posted" success page.""" doc = """Display a "comment was posted" success page."""

View File

@ -5,7 +5,9 @@ from django.contrib.auth.decorators import login_required, permission_required
from utils import next_redirect, confirmation_view from utils import next_redirect, confirmation_view
from django.contrib import comments from django.contrib import comments
from django.contrib.comments import signals from django.contrib.comments import signals
from django.contrib.csrf.decorators import csrf_protect
@csrf_protect
@login_required @login_required
def flag(request, comment_id, next=None): def flag(request, comment_id, next=None):
""" """
@ -30,6 +32,7 @@ def flag(request, comment_id, next=None):
template.RequestContext(request) template.RequestContext(request)
) )
@csrf_protect
@permission_required("comments.can_moderate") @permission_required("comments.can_moderate")
def delete(request, comment_id, next=None): def delete(request, comment_id, next=None):
""" """
@ -56,6 +59,7 @@ def delete(request, comment_id, next=None):
template.RequestContext(request) template.RequestContext(request)
) )
@csrf_protect
@permission_required("comments.can_moderate") @permission_required("comments.can_moderate")
def approve(request, comment_id, next=None): def approve(request, comment_id, next=None):
""" """

View File

@ -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() }

View File

@ -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.
"""

View File

@ -5,90 +5,209 @@ This module provides a middleware that implements protection
against request forgeries from other sites. against request forgeries from other sites.
""" """
import re
import itertools import itertools
import re
import random
try: try:
from functools import wraps from functools import wraps
except ImportError: except ImportError:
from django.utils.functional import wraps # Python 2.3, 2.4 fallback. from django.utils.functional import wraps # Python 2.3, 2.4 fallback.
from django.conf import settings 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.hashcompat import md5_constructor
from django.utils.safestring import mark_safe 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 = \ _POST_FORM_RE = \
re.compile(r'(<form\W[^>]*\bmethod\s*=\s*(\'|"|)POST(\'|"|)\b[^>]*>)', re.IGNORECASE) re.compile(r'(<form\W[^>]*\bmethod\s*=\s*(\'|"|)POST(\'|"|)\b[^>]*>)', re.IGNORECASE)
_HTML_TYPES = ('text/html', 'application/xhtml+xml') _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() 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): class CsrfViewMiddleware(object):
""" """
Middleware that requires a present and correct csrfmiddlewaretoken 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): def process_view(self, request, callback, callback_args, callback_kwargs):
if request.method == 'POST':
if getattr(callback, 'csrf_exempt', False): if getattr(callback, 'csrf_exempt', False):
return None return None
if request.is_ajax(): if getattr(request, 'csrf_processing_done', False):
return None return None
reject = lambda s: _get_failure_view()(request, reason=s)
def accept():
# Avoid checking the request twice by adding a custom attribute to
# request. This will be relevant when both decorator and middleware
# are used.
request.csrf_processing_done = True
return None
# If the user doesn't have a CSRF cookie, generate one and store it in the
# request, so it's available to the view. We'll store it in a cookie when
# we reach the response.
try:
request.META["CSRF_COOKIE"] = request.COOKIES[settings.CSRF_COOKIE_NAME]
cookie_is_new = False
except KeyError:
# No cookie, so create one.
request.META["CSRF_COOKIE"] = _get_new_csrf_key()
cookie_is_new = True
if request.method == 'POST':
if getattr(request, '_dont_enforce_csrf_checks', False):
# Mechanism to turn off CSRF checks for test suite. It comes after
# the creation of CSRF cookies, so that everything else continues to
# work exactly the same (e.g. cookies are sent etc), but before the
# any branches that call reject()
return accept()
if request.is_ajax():
# .is_ajax() is based on the presence of X-Requested-With. In
# the context of a browser, this can only be sent if using
# XmlHttpRequest. Browsers implement careful policies for
# XmlHttpRequest:
#
# * Normally, only same-domain requests are allowed.
#
# * Some browsers (e.g. Firefox 3.5 and later) relax this
# carefully:
#
# * if it is a 'simple' GET or POST request (which can
# include no custom headers), it is allowed to be cross
# domain. These requests will not be recognized as AJAX.
#
# * if a 'preflight' check with the server confirms that the
# server is expecting and allows the request, cross domain
# requests even with custom headers are allowed. These
# requests will be recognized as AJAX, but can only get
# through when the developer has specifically opted in to
# allowing the cross-domain POST request.
#
# So in all cases, it is safe to allow these requests through.
return accept()
if request.is_secure():
# Strict referer checking for HTTPS
referer = request.META.get('HTTP_REFERER')
if referer is None:
return reject("Referer checking failed - no Referer.")
# The following check ensures that the referer is HTTPS,
# the domains match and the ports match. This might be too strict.
good_referer = 'https://%s/' % request.get_host()
if not referer.startswith(good_referer):
return reject("Referer checking failed - %s does not match %s." %
(referer, good_referer))
# If the user didn't already have a CSRF key, then accept the
# session key for the middleware token, so CSRF protection isn't lost
# for the period between upgrading to CSRF cookes to the first time
# each user comes back to the site to receive one.
if cookie_is_new:
try: try:
session_id = request.COOKIES[settings.SESSION_COOKIE_NAME] session_id = request.COOKIES[settings.SESSION_COOKIE_NAME]
csrf_token = _make_legacy_session_token(session_id)
except KeyError: except KeyError:
# No session, no check required # No CSRF cookie and no session cookie. For POST requests,
return None # we insist on a CSRF cookie, and in this way we can avoid
# all CSRF attacks, including login CSRF.
return reject("No CSRF cookie.")
else:
csrf_token = request.META["CSRF_COOKIE"]
csrf_token = _make_token(session_id)
# check incoming token # check incoming token
try: request_csrf_token = request.POST.get('csrfmiddlewaretoken', None)
request_csrf_token = request.POST['csrfmiddlewaretoken']
except KeyError:
return HttpResponseForbidden(_ERROR_MSG)
if request_csrf_token != csrf_token: if request_csrf_token != csrf_token:
return HttpResponseForbidden(_ERROR_MSG) return reject("CSRF token missing or incorrect.")
return None return accept()
def process_response(self, request, response):
if getattr(response, 'csrf_processing_done', False):
return response
# If CSRF_COOKIE is unset, then CsrfViewMiddleware.process_view was
# never called, probaby because a request middleware returned a response
# (for example, contrib.auth redirecting to a login page).
if request.META.get("CSRF_COOKIE") is None:
return response
if not request.META.get("CSRF_COOKIE_USED", False):
return response
# Set the CSRF cookie even if it's already set, so we renew the expiry timer.
response.set_cookie(settings.CSRF_COOKIE_NAME,
request.META["CSRF_COOKIE"], max_age = 60 * 60 * 24 * 7 * 52,
domain=settings.CSRF_COOKIE_DOMAIN)
# Content varies with the CSRF cookie, so set the Vary header.
patch_vary_headers(response, ('Cookie',))
response.csrf_processing_done = True
return response
class CsrfResponseMiddleware(object): class CsrfResponseMiddleware(object):
""" """
Middleware that post-processes a response to add a DEPRECATED
csrfmiddlewaretoken if the response/request have an active Middleware that post-processes a response to add a csrfmiddlewaretoken.
session.
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): def process_response(self, request, response):
if getattr(response, 'csrf_exempt', False): if getattr(response, 'csrf_exempt', False):
return response return response
csrf_token = None if response['Content-Type'].split(';')[0] in _HTML_TYPES:
try: csrf_token = get_token(request)
# This covers a corner case in which the outgoing response # If csrf_token is None, we have no token for this request, which probably
# both contains a form and sets a session cookie. This # means that this is a response from a request middleware.
# really should not be needed, since it is best if views if csrf_token is None:
# that create a new session (login pages) also do a return response
# 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:
# ensure we don't add the 'id' attribute twice (HTML validity) # ensure we don't add the 'id' attribute twice (HTML validity)
idattributes = itertools.chain(("id='csrfmiddlewaretoken'",), idattributes = itertools.chain(("id='csrfmiddlewaretoken'",),
@ -103,34 +222,43 @@ class CsrfResponseMiddleware(object):
# Modify any POST forms # Modify any POST forms
response.content, n = _POST_FORM_RE.subn(add_csrf_field, response.content) response.content, n = _POST_FORM_RE.subn(add_csrf_field, response.content)
if n > 0: 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 # 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 # the Etag was set by CommonMiddleware. The safest thing is just
# to delete. See bug #9163 # to delete. See bug #9163
del response['ETag'] del response['ETag']
return response return response
class CsrfMiddleware(CsrfViewMiddleware, CsrfResponseMiddleware): class CsrfMiddleware(object):
"""Django middleware that adds protection against Cross Site """
Django middleware that adds protection against Cross Site
Request Forgeries by adding hidden form fields to POST forms and Request Forgeries by adding hidden form fields to POST forms and
checking requests for the correct value. checking requests for the correct value.
In the list of middlewares, SessionMiddleware is required, and CsrfMiddleware uses two middleware, CsrfViewMiddleware and
must come after this middleware. CsrfMiddleWare must come after CsrfResponseMiddleware, which can be used independently. It is recommended
compression middleware. to use only CsrfViewMiddleware and use the csrf_token template tag in
templates for inserting the token.
If a session ID cookie is present, it is hashed with the
SECRET_KEY setting to create an authentication token. This token
is added to all outgoing POST forms and is expected on all
incoming POST requests that have a session ID cookie.
If you are setting cookies directly, instead of using Django's
session framework, this middleware will not work.
CsrfMiddleWare is composed of two middleware, CsrfViewMiddleware
and CsrfResponseMiddleware which can be used independently.
""" """
pass # We can't just inherit from CsrfViewMiddleware and CsrfResponseMiddleware
# because both have process_response methods.
def __init__(self):
self.response_middleware = CsrfResponseMiddleware()
self.view_middleware = CsrfViewMiddleware()
def process_response(self, request, resp):
# We must do the response post-processing first, because that calls
# get_token(), which triggers a flag saying that the CSRF cookie needs
# to be sent (done in CsrfViewMiddleware.process_response)
resp2 = self.response_middleware.process_response(request, resp)
return self.view_middleware.process_response(request, resp2)
def process_view(self, request, callback, callback_args, callback_kwargs):
return self.view_middleware.process_view(request, callback, callback_args,
callback_kwargs)
def csrf_response_exempt(view_func): def csrf_response_exempt(view_func):
""" """

View File

@ -1,144 +1,323 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.test import TestCase from django.test import TestCase
from django.http import HttpRequest, HttpResponse, HttpResponseForbidden from django.http import HttpRequest, HttpResponse
from django.contrib.csrf.middleware import CsrfMiddleware, _make_token, csrf_exempt 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.conf import settings
from django.template import RequestContext, Template
# Response/views used for CsrfResponseMiddleware and CsrfViewMiddleware tests
def post_form_response(): def post_form_response():
resp = HttpResponse(content=""" resp = HttpResponse(content="""
<html><body><form method="POST"><input type="text" /></form></body></html> <html><body><form method="POST"><input type="text" /></form></body></html>
""", mimetype="text/html") """, mimetype="text/html")
return resp 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() 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): 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" _session_id = "1"
_secret_key_for_session_test= "test"
def _get_GET_no_session_request(self): def _get_GET_no_csrf_cookie_request(self):
return HttpRequest() return TestingHttpRequest()
def _get_GET_session_request(self): def _get_GET_csrf_cookie_request(self):
req = self._get_GET_no_session_request() req = TestingHttpRequest()
req.COOKIES[settings.SESSION_COOKIE_NAME] = self._session_id req.COOKIES[settings.CSRF_COOKIE_NAME] = self._csrf_id
return req return req
def _get_POST_session_request(self): def _get_POST_csrf_cookie_request(self):
req = self._get_GET_session_request() req = self._get_GET_csrf_cookie_request()
req.method = "POST" req.method = "POST"
return req return req
def _get_POST_no_session_request(self): def _get_POST_no_csrf_cookie_request(self):
req = self._get_GET_no_session_request() req = self._get_GET_no_csrf_cookie_request()
req.method = "POST" req.method = "POST"
return req return req
def _get_POST_request_with_token(self):
req = self._get_POST_csrf_cookie_request()
req.POST['csrfmiddlewaretoken'] = self._csrf_id
return req
def _get_POST_session_request_with_token(self): def _get_POST_session_request_with_token(self):
req = self._get_POST_session_request() req = self._get_POST_no_csrf_cookie_request()
req.POST['csrfmiddlewaretoken'] = _make_token(self._session_id) req.COOKIES[settings.SESSION_COOKIE_NAME] = self._session_id
req.POST['csrfmiddlewaretoken'] = self._session_token
return req return req
def _get_post_form_response(self): def _get_POST_session_request_no_token(self):
return post_form_response() req = self._get_POST_no_csrf_cookie_request()
req.COOKIES[settings.SESSION_COOKIE_NAME] = self._session_id
return req
def _get_new_session_response(self): def _check_token_present(self, response, csrf_id=None):
resp = self._get_post_form_response() self.assertContains(response, "name='csrfmiddlewaretoken' value='%s'" % (csrf_id or self._csrf_id))
resp.cookies[settings.SESSION_COOKIE_NAME] = self._session_id
return resp
def _check_token_present(self, response): # Check the post processing and outgoing cookie
self.assertContains(response, "name='csrfmiddlewaretoken' value='%s'" % _make_token(self._session_id)) def test_process_response_no_csrf_cookie(self):
def get_view(self):
return test_view
# Check the post processing
def test_process_response_no_session(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() req = self._get_GET_no_csrf_cookie_request()
resp = self._get_post_form_response() CsrfMiddleware().process_view(req, post_form_view, (), {})
resp = post_form_response()
resp_content = resp.content # needed because process_response modifies resp
resp2 = CsrfMiddleware().process_response(req, resp)
csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False)
self.assertNotEqual(csrf_cookie, False)
self.assertNotEqual(resp_content, resp2.content)
self._check_token_present(resp2, csrf_cookie.value)
# Check the Vary header got patched correctly
self.assert_('Cookie' in resp2.get('Vary',''))
def test_process_response_no_csrf_cookie_view_only_get_token_used(self):
"""
When no prior CSRF cookie exists, check that the cookie is created, even
if only CsrfViewMiddleware is used.
"""
# This is checking that CsrfViewMiddleware has the cookie setting
# code. Most of the other tests use CsrfMiddleware.
req = self._get_GET_no_csrf_cookie_request()
# token_view calls get_token() indirectly
CsrfViewMiddleware().process_view(req, token_view, (), {})
resp = token_view(req)
resp2 = CsrfViewMiddleware().process_response(req, resp)
csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False)
self.assertNotEqual(csrf_cookie, False)
def test_process_response_get_token_not_used(self):
"""
Check that if get_token() is not called, the view middleware does not
add a cookie.
"""
# This is important to make pages cacheable. Pages which do call
# get_token(), assuming they use the token, are not cacheable because
# the token is specific to the user
req = self._get_GET_no_csrf_cookie_request()
# non_token_view_using_request_processor does not call get_token(), but
# does use the csrf request processor. By using this, we are testing
# that the view processor is properly lazy and doesn't call get_token()
# until needed.
CsrfViewMiddleware().process_view(req, non_token_view_using_request_processor, (), {})
resp = non_token_view_using_request_processor(req)
resp2 = CsrfViewMiddleware().process_response(req, resp)
csrf_cookie = resp2.cookies.get(settings.CSRF_COOKIE_NAME, False)
self.assertEqual(csrf_cookie, False)
def test_process_response_existing_csrf_cookie(self):
"""
Check that the token is inserted when a prior CSRF cookie exists
"""
req = self._get_GET_csrf_cookie_request()
CsrfMiddleware().process_view(req, post_form_view, (), {})
resp = post_form_response()
resp_content = resp.content # needed because process_response modifies resp
resp2 = CsrfMiddleware().process_response(req, resp)
self.assertNotEqual(resp_content, resp2.content)
self._check_token_present(resp2)
def test_process_response_non_html(self):
"""
Check the the post-processor does nothing for content-types not in _HTML_TYPES.
"""
req = self._get_GET_no_csrf_cookie_request()
CsrfMiddleware().process_view(req, post_form_view, (), {})
resp = post_form_response_non_html()
resp_content = resp.content # needed because process_response modifies resp resp_content = resp.content # needed because process_response modifies resp
resp2 = CsrfMiddleware().process_response(req, resp) resp2 = CsrfMiddleware().process_response(req, resp)
self.assertEquals(resp_content, resp2.content) self.assertEquals(resp_content, resp2.content)
def test_process_response_existing_session(self):
"""
Check that the token is inserted if there is an existing session
"""
req = self._get_GET_session_request()
resp = self._get_post_form_response()
resp_content = resp.content # needed because process_response modifies resp
resp2 = CsrfMiddleware().process_response(req, resp)
self.assertNotEqual(resp_content, resp2.content)
self._check_token_present(resp2)
def test_process_response_new_session(self):
"""
Check that the token is inserted if there is a new session being started
"""
req = self._get_GET_no_session_request() # no session in request
resp = self._get_new_session_response() # but new session started
resp_content = resp.content # needed because process_response modifies resp
resp2 = CsrfMiddleware().process_response(req, resp)
self.assertNotEqual(resp_content, resp2.content)
self._check_token_present(resp2)
def test_process_response_exempt_view(self): def test_process_response_exempt_view(self):
""" """
Check that no post processing is done for an exempt view Check that no post processing is done for an exempt view
""" """
req = self._get_POST_session_request() req = self._get_POST_csrf_cookie_request()
resp = csrf_exempt(self.get_view())(req) resp = csrf_exempt(post_form_view)(req)
resp_content = resp.content resp_content = resp.content
resp2 = CsrfMiddleware().process_response(req, resp) resp2 = CsrfMiddleware().process_response(req, resp)
self.assertEquals(resp_content, resp2.content) self.assertEquals(resp_content, resp2.content)
# Check the request processing # 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. Check that if neither a CSRF cookie nor a session cookie are present,
to the incoming request. the middleware rejects the incoming request. This will stop login CSRF.
""" """
req = self._get_POST_no_session_request() req = self._get_POST_no_csrf_cookie_request()
req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {}) req2 = CsrfMiddleware().process_view(req, post_form_view, (), {})
self.assertEquals(403, req2.status_code)
def test_process_request_csrf_cookie_no_token(self):
"""
Check that if a CSRF cookie is present but no token, the middleware
rejects the incoming request.
"""
req = self._get_POST_csrf_cookie_request()
req2 = CsrfMiddleware().process_view(req, post_form_view, (), {})
self.assertEquals(403, req2.status_code)
def test_process_request_csrf_cookie_and_token(self):
"""
Check that if both a cookie and a token is present, the middleware lets it through.
"""
req = self._get_POST_request_with_token()
req2 = CsrfMiddleware().process_view(req, post_form_view, (), {})
self.assertEquals(None, req2) self.assertEquals(None, req2)
def test_process_request_session_no_token(self): def test_process_request_session_cookie_no_csrf_cookie_token(self):
""" """
Check that if a session is present but no token, we get a 'forbidden' When no CSRF cookie exists, but the user has a session, check that a token
""" using the session cookie as a legacy CSRF cookie is accepted.
req = self._get_POST_session_request()
req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {})
self.assertEquals(HttpResponseForbidden, req2.__class__)
def test_process_request_session_and_token(self):
"""
Check that if a session is present and a token, the middleware lets it through
""" """
orig_secret_key = settings.SECRET_KEY
settings.SECRET_KEY = self._secret_key_for_session_test
try:
req = self._get_POST_session_request_with_token() req = self._get_POST_session_request_with_token()
req2 = CsrfMiddleware().process_view(req, self.get_view(), (), {}) req2 = CsrfMiddleware().process_view(req, post_form_view, (), {})
self.assertEquals(None, req2) self.assertEquals(None, req2)
finally:
settings.SECRET_KEY = orig_secret_key
def test_process_request_session_no_token_exempt_view(self): def test_process_request_session_cookie_no_csrf_cookie_no_token(self):
""" """
Check that if a session is present and no token, but the csrf_exempt Check that if a session cookie is present but no token and no CSRF cookie,
the request is rejected.
"""
req = self._get_POST_session_request_no_token()
req2 = CsrfMiddleware().process_view(req, post_form_view, (), {})
self.assertEquals(403, req2.status_code)
def test_process_request_csrf_cookie_no_token_exempt_view(self):
"""
Check that if a CSRF cookie is present and no token, but the csrf_exempt
decorator has been applied to the view, the middleware lets it through decorator has been applied to the view, the middleware lets it through
""" """
req = self._get_POST_session_request() req = self._get_POST_csrf_cookie_request()
req2 = CsrfMiddleware().process_view(req, csrf_exempt(self.get_view()), (), {}) req2 = CsrfMiddleware().process_view(req, csrf_exempt(post_form_view), (), {})
self.assertEquals(None, req2) self.assertEquals(None, req2)
def test_ajax_exemption(self): def test_ajax_exemption(self):
""" """
Check that AJAX requests are automatically exempted. 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' 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) self.assertEquals(None, req2)

View File

@ -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')

View File

@ -4,7 +4,7 @@
{% if form.errors %}<h1>Please correct the following errors</h1>{% else %}<h1>Submit</h1>{% endif %} {% 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> <table>
{{ form }} {{ form }}
</table> </table>

View File

@ -15,7 +15,7 @@
<p>Security hash: {{ hash_value }}</p> <p>Security hash: {{ hash_value }}</p>
<form action="" method="post"> <form action="" method="post">{% csrf_token %}
{% for field in form %}{{ field.as_hidden }} {% for field in form %}{{ field.as_hidden }}
{% endfor %} {% endfor %}
<input type="hidden" name="{{ stage_field }}" value="2" /> <input type="hidden" name="{{ stage_field }}" value="2" />
@ -25,7 +25,7 @@
<h1>Or edit it again</h1> <h1>Or edit it again</h1>
<form action="" method="post"> <form action="" method="post">{% csrf_token %}
<table> <table>
{{ form }} {{ form }}
</table> </table>

View File

@ -147,15 +147,18 @@ class WizardPageTwoForm(forms.Form):
class WizardClass(wizard.FormWizard): class WizardClass(wizard.FormWizard):
def render_template(self, *args, **kw): def render_template(self, *args, **kw):
return "" return http.HttpResponse("")
def done(self, request, cleaned_data): def done(self, request, cleaned_data):
return http.HttpResponse(success_string) return http.HttpResponse(success_string)
class DummyRequest(object): class DummyRequest(http.HttpRequest):
def __init__(self, POST=None): def __init__(self, POST=None):
super(DummyRequest, self).__init__()
self.method = POST and "POST" or "GET" 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): class WizardTests(TestCase):
def test_step_starts_at_zero(self): def test_step_starts_at_zero(self):

View File

@ -14,6 +14,7 @@ from django.template.context import RequestContext
from django.utils.hashcompat import md5_constructor from django.utils.hashcompat import md5_constructor
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.contrib.formtools.utils import security_hash from django.contrib.formtools.utils import security_hash
from django.contrib.csrf.decorators import csrf_protect
class FormWizard(object): class FormWizard(object):
# Dictionary of extra template context variables. # Dictionary of extra template context variables.
@ -44,6 +45,7 @@ class FormWizard(object):
# hook methods might alter self.form_list. # hook methods might alter self.form_list.
return len(self.form_list) return len(self.form_list)
@csrf_protect
def __call__(self, request, *args, **kwargs): def __call__(self, request, *args, **kwargs):
""" """
Main method that does all the hard work, conforming to the Django view Main method that does all the hard work, conforming to the Django view

View File

@ -1,7 +1,12 @@
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.utils.importlib import import_module from django.utils.importlib import import_module
# Cache of actual callables.
_standard_context_processors = None _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): class ContextPopException(Exception):
"pop() has been called more times than push()" "pop() has been called more times than push()"
@ -75,7 +80,10 @@ def get_standard_processors():
global _standard_context_processors global _standard_context_processors
if _standard_context_processors is None: if _standard_context_processors is None:
processors = [] 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('.') i = path.rfind('.')
module, attr = path[:i], path[i+1:] module, attr = path[:i], path[i+1:]
try: try:

View File

@ -37,6 +37,23 @@ class CommentNode(Node):
def render(self, context): def render(self, context):
return '' 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): class CycleNode(Node):
def __init__(self, cyclevars, variable_name=None): def __init__(self, cyclevars, variable_name=None):
self.cycle_iter = itertools_cycle(cyclevars) self.cycle_iter = itertools_cycle(cyclevars)
@ -523,6 +540,10 @@ def cycle(parser, token):
return node return node
cycle = register.tag(cycle) cycle = register.tag(cycle)
def csrf_token(parser, token):
return CsrfTokenNode()
register.tag(csrf_token)
def debug(parser, token): def debug(parser, token):
""" """
Outputs a whole load of debugging information, including the current Outputs a whole load of debugging information, including the current

View File

@ -66,6 +66,11 @@ class ClientHandler(BaseHandler):
signals.request_started.send(sender=self.__class__) signals.request_started.send(sender=self.__class__)
try: try:
request = WSGIRequest(environ) 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) response = self.get_response(request)
# Apply response middleware. # Apply response middleware.

View File

@ -13,6 +13,12 @@ their deprecation, as per the :ref:`Django deprecation policy
hooking up admin URLs. This has been deprecated since the 1.1 hooking up admin URLs. This has been deprecated since the 1.1
release. release.
* 1.4
* ``CsrfResponseMiddleware``. This has been deprecated since the 1.2
release, in favour of the template tag method for inserting the CSRF
token. ``CsrfMiddleware``, which combines ``CsrfResponseMiddleware``
and ``CsrfViewMiddleware``, is also deprecated.
* 2.0 * 2.0
* ``django.views.defaults.shortcut()``. This function has been moved * ``django.views.defaults.shortcut()``. This function has been moved
to ``django.contrib.contenttypes.views.shortcut()`` as part of the to ``django.contrib.contenttypes.views.shortcut()`` as part of the

View File

@ -21,6 +21,7 @@ tutorial, so that the template contains an HTML ``<form>`` element:
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %} {% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}
<form action="/polls/{{ poll.id }}/vote/" method="post"> <form action="/polls/{{ poll.id }}/vote/" method="post">
{% csrf_token %}
{% for choice in poll.choice_set.all %} {% for choice in poll.choice_set.all %}
<input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}" /> <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}" />
<label for="choice{{ forloop.counter }}">{{ choice.choice }}</label><br /> <label for="choice{{ forloop.counter }}">{{ choice.choice }}</label><br />
@ -46,6 +47,28 @@ A quick rundown:
* ``forloop.counter`` indicates how many times the :ttag:`for` tag has gone * ``forloop.counter`` indicates how many times the :ttag:`for` tag has gone
through its loop through its loop
* Since we are creating a POST form (which can have the effect of modifying
data), we unfortunately need to worry about Cross Site Request Forgeries.
Thankfully, you don't have to worry too hard, because Django comes with
very easy-to-use system for protecting against it. In short, all POST
forms that are targetted at internal URLs need the ``{% csrf_token %}``
template tag adding.
The ``{% csrf_token %}`` tag requires information from the request object, which
is not normally accessible from within the template context. To fix this, a
small adjustment needs to be made to the ``detail`` view, so that it looks like
the following::
from django.template import RequestContext
# ...
def detail(request, poll_id):
p = get_object_or_404(Poll, pk=poll_id)
return render_to_response('polls/detail.html', {'poll': p},
context_instance=RequestContext(request))
The details of how this works are explained in the documentation for
:ref:`RequestContext <subclassing-context-requestcontext>`.
Now, let's create a Django view that handles the submitted data and does Now, let's create a Django view that handles the submitted data and does
something with it. Remember, in :ref:`Tutorial 3 <intro-tutorial03>`, we something with it. Remember, in :ref:`Tutorial 3 <intro-tutorial03>`, we
created a URLconf for the polls application that includes this line:: created a URLconf for the polls application that includes this line::
@ -58,6 +81,7 @@ create a real version. Add the following to ``mysite/polls/views.py``::
from django.shortcuts import get_object_or_404, render_to_response from django.shortcuts import get_object_or_404, render_to_response
from django.http import HttpResponseRedirect, HttpResponse from django.http import HttpResponseRedirect, HttpResponse
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.template import RequestContext
from mysite.polls.models import Choice, Poll from mysite.polls.models import Choice, Poll
# ... # ...
def vote(request, poll_id): def vote(request, poll_id):
@ -69,7 +93,7 @@ create a real version. Add the following to ``mysite/polls/views.py``::
return render_to_response('polls/detail.html', { return render_to_response('polls/detail.html', {
'poll': p, 'poll': p,
'error_message': "You didn't select a choice.", 'error_message': "You didn't select a choice.",
}) }, context_instance=RequestContext(request))
else: else:
selected_choice.votes += 1 selected_choice.votes += 1
selected_choice.save() selected_choice.save()

View File

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

View File

@ -177,7 +177,7 @@ Here's a full example template:
{% block content %} {% block content %}
<p>Step {{ step }} of {{ step_count }}</p> <p>Step {{ step }} of {{ step_count }}</p>
<form action="." method="post"> <form action="." method="post">{% csrf_token %}
<table> <table>
{{ form }} {{ form }}
</table> </table>

View File

@ -144,6 +144,44 @@ Default: ``600``
The default number of seconds to cache a page when the caching middleware or The default number of seconds to cache a page when the caching middleware or
``cache_page()`` decorator is used. ``cache_page()`` decorator is used.
.. setting:: CSRF_COOKIE_NAME
CSRF_COOKIE_NAME
----------------
Default: ``'csrftoken'``
The name of the cookie to use for the CSRF authentication token. This can be whatever you
want. See :ref:`ref-contrib-csrf`.
.. setting:: CSRF_COOKIE_DOMAIN
CSRF_COOKIE_DOMAIN
------------------
Default: ``None``
The domain to be used when setting the CSRF cookie. This can be useful for
allowing cross-subdomain requests to be exluded from the normal cross site
request forgery protection. It should be set to a string such as
``".lawrence.com"`` to allow a POST request from a form on one subdomain to be
accepted by accepted by a view served from another subdomain.
.. setting:: CSRF_FAILURE_VIEW
CSRF_FAILURE_VIEW
-----------------
Default: ``'django.contrib.csrf.views.csrf_failure'``
A dotted path to the view function to be used when an incoming request
is rejected by the CSRF protection. The function should have this signature::
def csrf_failure(request, reason="")
where ``reason`` is a short message (intended for developers or logging, not for
end users) indicating the reason the request was rejected. See
:ref:`ref-contrib-csrf`.
.. setting:: DATABASE_ENGINE .. setting:: DATABASE_ENGINE
DATABASE_ENGINE DATABASE_ENGINE
@ -751,6 +789,7 @@ Default::
('django.middleware.common.CommonMiddleware', ('django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.csrf.middleware.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',) 'django.contrib.auth.middleware.AuthenticationMiddleware',)
A tuple of middleware classes to use. See :ref:`topics-http-middleware`. A tuple of middleware classes to use. See :ref:`topics-http-middleware`.

View File

@ -313,6 +313,13 @@ and return a dictionary of items to be merged into the context. By default,
"django.core.context_processors.i18n", "django.core.context_processors.i18n",
"django.core.context_processors.media") "django.core.context_processors.media")
.. versionadded:: 1.2
In addition to these, ``RequestContext`` always uses
``'django.contrib.csrf.context_processors.csrf'``. This is a security
related context processor required by the admin and other contrib apps, and,
in case of accidental misconfiguration, it is deliberately hardcoded in and
cannot be turned off by the :setting:`TEMPLATE_CONTEXT_PROCESSORS` setting.
Each processor is applied in order. That means, if one processor adds a Each processor is applied in order. That means, if one processor adds a
variable to the context and a second processor adds a variable with the same variable to the context and a second processor adds a variable with the same
name, the second will override the first. The default processors are explained name, the second will override the first. The default processors are explained
@ -404,6 +411,14 @@ If :setting:`TEMPLATE_CONTEXT_PROCESSORS` contains this processor, every
``RequestContext`` will contain a variable ``MEDIA_URL``, providing the ``RequestContext`` will contain a variable ``MEDIA_URL``, providing the
value of the :setting:`MEDIA_URL` setting. value of the :setting:`MEDIA_URL` setting.
django.contrib.csrf.context_processors.csrf
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. versionadded:: 1.2
This processor adds a token that is needed by the ``csrf_token`` template tag
for protection against :ref:`Cross Site Request Forgeries <ref-contrib-csrf>`.
django.core.context_processors.request django.core.context_processors.request
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -53,6 +53,13 @@ Ignore everything between ``{% comment %}`` and ``{% endcomment %}``
.. templatetag:: cycle .. templatetag:: cycle
csrf_token
~~~~~~~~~~
.. versionadded:: 1.2
This is described in the documentation for :ref:`Cross Site Request Forgeries <ref-contrib-csrf>`.
cycle cycle
~~~~~ ~~~~~

View File

@ -2,6 +2,22 @@
Backwards-incompatible changes Backwards-incompatible changes
============================== ==============================
CSRF Protection
---------------
There have been large changes to the way that CSRF protection works, detailed in
:ref:`the CSRF documentaton <ref-contrib-csrf>`. The following are the major
changes that developers must be aware of:
* ``CsrfResponseMiddleware`` and ``CsrfMiddleware`` have been deprecated, and
will be removed completely in Django 1.4, in favour of a template tag that
should be inserted into forms.
* ``CsrfViewMiddleware`` is included in :setting:`MIDDLEWARE_CLASSES` by
default. This turns on CSRF protection by default, so that views that accept
POST requests need to be written to work with the middleware. Instructions
on how to do this are found in the CSRF docs.
LazyObject LazyObject
---------- ----------

View File

@ -767,7 +767,7 @@ the following line to your URLconf::
<p>Your username and password didn't match. Please try again.</p> <p>Your username and password didn't match. Please try again.</p>
{% endif %} {% endif %}
<form method="post" action="{% url django.contrib.auth.views.login %}"> <form method="post" action="{% url django.contrib.auth.views.login %}">{% csrf_token %}
<table> <table>
<tr> <tr>
<td>{{ form.username.label_tag }}</td> <td>{{ form.username.label_tag }}</td>

View File

@ -29,6 +29,7 @@ created by :djadmin:`django-admin.py startproject <startproject>`::
MIDDLEWARE_CLASSES = ( MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.csrf.middleware.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
) )

View File

@ -0,0 +1,369 @@
#!/usr/bin/env python
# This script aims to help developers locate forms and view code that needs to
# use the new CSRF protection in Django 1.2. It tries to find all the code that
# may need the steps described in the CSRF documentation. It does not modify
# any code directly, it merely attempts to locate it. Developers should be
# aware of its limitations, described below.
#
# For each template that contains at least one POST form, the following info is printed:
#
# <Absolute path to template>
# AKA: <Aliases (relative to template directory/directories that contain it)>
# POST forms: <Number of POST forms>
# With token: <Number of POST forms with the CSRF token already added>
# Without token:
# <File name and line number of form without token>
#
# Searching for:
# <Template names that need to be searched for in view code
# (includes templates that 'include' current template)>
#
# Found:
# <File name and line number of any view code found>
#
# The format used allows this script to be used in Emacs grep mode:
# M-x grep
# Run grep (like this): /path/to/my/virtualenv/python /path/to/django/src/extras/csrf_migration_helper.py --settings=mysettings /path/to/my/srcs
# Limitations
# ===========
#
# - All templates must be stored on disk in '.html' or '.htm' files.
# (extensions configurable below)
#
# - All Python code must be stored on disk in '.py' files. (extensions
# configurable below)
#
# - All templates must be accessible from TEMPLATE_DIRS or from the 'templates/'
# directory in apps specified in INSTALLED_APPS. Non-file based template
# loaders are out of the picture, because there is no way to ask them to
# return all templates.
#
# - If you put the {% csrf_token %} tag on the same line as the <form> tag it
# will be detected, otherwise it will be assumed that the form does not have
# the token.
#
# - It's impossible to programmatically determine which forms should and should
# not have the token added. The developer must decide when to do this,
# ensuring that the token is only added to internally targetted forms.
#
# - It's impossible to programmatically work out when a template is used. The
# attempts to trace back to view functions are guesses, and could easily fail
# in the following ways:
#
# * If the 'include' template tag is used with a variable
# i.e. {% include tname %} where tname is a variable containing the actual
# template name, rather than {% include "my_template.html" %}.
#
# * If the template name has been built up by view code instead of as a simple
# string. For example, generic views and the admin both do this. (These
# apps are both contrib and both use RequestContext already, as it happens).
#
# * If the 'ssl' tag (or any template tag other than 'include') is used to
# include the template in another template.
#
# - All templates belonging to apps referenced in INSTALLED_APPS will be
# searched, which may include third party apps or Django contrib. In some
# cases, this will be a good thing, because even if the templates of these
# apps have been fixed by someone else, your own view code may reference the
# same template and may need to be updated.
#
# You may, however, wish to comment out some entries in INSTALLED_APPS or
# TEMPLATE_DIRS before running this script.
# Improvements to this script are welcome!
# Configuration
# =============
TEMPLATE_EXTENSIONS = [
".html",
".htm",
]
PYTHON_SOURCE_EXTENSIONS = [
".py",
]
TEMPLATE_ENCODING = "UTF-8"
PYTHON_ENCODING = "UTF-8"
# Method
# ======
# Find templates:
# - template dirs
# - installed apps
#
# Search for POST forms
# - Work out what the name of the template is, as it would appear in an
# 'include' or get_template() call. This can be done by comparing template
# filename to all template dirs. Some templates can have more than one
# 'name' e.g. if a directory and one of its child directories are both in
# TEMPLATE_DIRS. This is actually a common hack used for
# overriding-and-extending admin templates.
#
# For each POST form,
# - see if it already contains '{% csrf_token %}' immediately after <form>
# - work back to the view function(s):
# - First, see if the form is included in any other templates, then
# recursively compile a list of affected templates.
# - Find any code function that references that template. This is just a
# brute force text search that can easily return false positives
# and fail to find real instances.
import os
import sys
import re
try:
set
except NameError:
from sets import Set as set
USAGE = """
This tool helps to locate forms that need CSRF tokens added and the
corresponding view code. This processing is NOT fool proof, and you should read
the help contained in the script itself. Also, this script may need configuring
(by editing the script) before use.
Usage:
python csrf_migration_helper.py [--settings=path.to.your.settings] /path/to/python/code [more paths...]
Paths can be specified as relative paths.
With no arguments, this help is printed.
"""
_POST_FORM_RE = \
re.compile(r'(<form\W[^>]*\bmethod\s*=\s*(\'|"|)POST(\'|"|)\b[^>]*>)', re.IGNORECASE)
_TOKEN_RE = re.compile('\{% csrf_token')
def get_template_dirs():
"""
Returns a set of all directories that contain project templates.
"""
from django.conf import settings
dirs = set()
if 'django.template.loaders.filesystem.load_template_source' in settings.TEMPLATE_LOADERS:
dirs.update(map(unicode, settings.TEMPLATE_DIRS))
if 'django.template.loaders.app_directories.load_template_source' in settings.TEMPLATE_LOADERS:
from django.template.loaders.app_directories import app_template_dirs
dirs.update(app_template_dirs)
return dirs
def make_template_info(filename, root_dirs):
"""
Creates a Template object for a filename, calculating the possible
relative_filenames from the supplied filename and root template directories
"""
return Template(filename,
[filename[len(d)+1:] for d in root_dirs if filename.startswith(d)])
class Template(object):
def __init__(self, absolute_filename, relative_filenames):
self.absolute_filename, self.relative_filenames = absolute_filename, relative_filenames
def content(self):
try:
return self._content
except AttributeError:
fd = open(self.absolute_filename)
content = fd.read().decode(TEMPLATE_ENCODING)
fd.close()
self._content = content
return content
content = property(content)
def post_form_info(self):
"""
Get information about any POST forms in the template.
Returns [(linenumber, csrf_token added)]
"""
matches = []
for ln, line in enumerate(self.content.split("\n")):
m = _POST_FORM_RE.search(line)
if m is not None:
matches.append((ln + 1, _TOKEN_RE.search(line) is not None))
return matches
def includes_template(self, t):
"""
Returns true if this template includes template 't' (via {% include %})
"""
for r in t.relative_filenames:
if re.search(r'\{%\s*include\s+"' + re.escape(r) + r'"\s*%\}', self.content):
return True
return False
def related_templates(self):
"""
Returns all templates that include this one, recursively. (starting
with this one)
"""
try:
return self._related_templates
except AttributeError:
pass
retval = set([self])
for r in self.relative_filenames:
for t in self.all_templates:
if t.includes_template(self):
# If two templates mutually include each other, directly or
# indirectly, we have a problem here...
retval = retval.union(t.related_templates())
self._related_templates = retval
return retval
def __repr__(self):
return repr(self.absolute_filename)
def __eq__(self, other):
return self.absolute_filename == other.absolute_filename
def __hash__(self):
return hash(self.absolute_filename)
def get_templates(dirs):
"""
Returns all files in dirs that have template extensions, as Template
objects.
"""
templates = set()
for root in dirs:
for (dirpath, dirnames, filenames) in os.walk(root):
for f in filenames:
if len([True for e in TEMPLATE_EXTENSIONS if f.endswith(e)]) > 0:
t = make_template_info(os.path.join(dirpath, f), dirs)
# templates need to be able to search others:
t.all_templates = templates
templates.add(t)
return templates
def get_python_code(paths):
"""
Returns all Python code, as a list of tuples, each one being:
(filename, list of lines)
"""
retval = []
for p in paths:
for (dirpath, dirnames, filenames) in os.walk(p):
for f in filenames:
if len([True for e in PYTHON_SOURCE_EXTENSIONS if f.endswith(e)]) > 0:
fn = os.path.join(dirpath, f)
fd = open(fn)
content = [l.decode(PYTHON_ENCODING) for l in fd.readlines()]
fd.close()
retval.append((fn, content))
return retval
def search_python_list(python_code, template_names):
"""
Searches python code for a list of template names.
Returns a list of tuples, each one being:
(filename, line number)
"""
retval = []
for tn in template_names:
retval.extend(search_python(python_code, tn))
retval = list(set(retval))
retval.sort()
return retval
def search_python(python_code, template_name):
"""
Searches Python code for a template name.
Returns a list of tuples, each one being:
(filename, line number)
"""
retval = []
for fn, content in python_code:
for ln, line in enumerate(content):
if ((u'"%s"' % template_name) in line) or \
((u"'%s'" % template_name) in line):
retval.append((fn, ln + 1))
return retval
def main(pythonpaths):
template_dirs = get_template_dirs()
templates = get_templates(template_dirs)
python_code = get_python_code(pythonpaths)
for t in templates:
# Logic
form_matches = t.post_form_info()
num_post_forms = len(form_matches)
form_lines_without_token = [ln for (ln, has_token) in form_matches if not has_token]
if num_post_forms == 0:
continue
to_search = [rf for rt in t.related_templates() for rf in rt.relative_filenames]
found = search_python_list(python_code, to_search)
# Display:
print t.absolute_filename
for r in t.relative_filenames:
print u" AKA %s" % r
print u" POST forms: %s" % num_post_forms
print u" With token: %s" % (num_post_forms - len(form_lines_without_token))
if form_lines_without_token:
print u" Without token:"
for ln in form_lines_without_token:
print "%s:%d:" % (t.absolute_filename, ln)
print
print u" Searching for:"
for r in to_search:
print u" " + r
print
print u" Found:"
if len(found) == 0:
print " Nothing"
else:
for fn, ln in found:
print "%s:%d:" % (fn, ln)
print
print "----"
if __name__ == '__main__':
# Hacky argument parsing, one day I'll learn OptParse...
args = list(sys.argv[1:])
if len(args) > 0:
if args[0] in ['--help', '-h', '-?', '--usage']:
print USAGE
sys.exit(0)
else:
if args[0].startswith('--settings='):
module = args[0][len('--settings='):]
os.environ["DJANGO_SETTINGS_MODULE"] = module
args = args[1:]
if args[0].startswith('-'):
print "Unknown option: %s" % args[0]
print USAGE
sys.exit(1)
pythonpaths = args
if os.environ.get("DJANGO_SETTINGS_MODULE", None) is None:
print "You need to set DJANGO_SETTINGS_MODULE or use the '--settings' parameter"
sys.exit(1)
if len(pythonpaths) == 0:
print "Unrecognised command: %s" % command
print USAGE
sys.exit(1)
main(pythonpaths)
else:
# no args
print USAGE
sys.exit(0)

View File

@ -885,8 +885,9 @@ class AdminViewListEditable(TestCase):
# 4 action inputs (3 regular checkboxes, 1 checkbox to select all) # 4 action inputs (3 regular checkboxes, 1 checkbox to select all)
# main form submit button = 1 # main form submit button = 1
# search field and search submit button = 2 # search field and search submit button = 2
# 6 + 2 + 1 + 2 = 11 inputs # CSRF field = 1
self.failUnlessEqual(response.content.count("<input"), 15) # 6 + 2 + 4 + 1 + 2 + 1 = 16 inputs
self.failUnlessEqual(response.content.count("<input"), 16)
# 1 select per object = 3 selects # 1 select per object = 3 selects
self.failUnlessEqual(response.content.count("<select"), 4) self.failUnlessEqual(response.content.count("<select"), 4)