Ticket #510: csrf-protection.patch

File csrf-protection.patch, 7.1 KB (added by Simon Willison, 10 years ago)

Patch implementing CSRF protection for Django admin screens.

  • models/auth.py

     
    222222    def _module_log_action(user_id, content_type_id, object_id, object_repr, action_flag, change_message=''):
    223223        e = LogEntry(None, None, user_id, content_type_id, object_id, object_repr[:200], action_flag, change_message)
    224224        e.save()
     225
     226class CsrfToken(meta.Model):
     227    token = meta.CharField(maxlength=200)
     228    issued = meta.DateTimeField(auto_now=True)
     229    user = meta.ForeignKey(User)
     230    class META:
     231        module_name = 'csrf'
     232        verbose_name_plural = 'CSRF tokens'
     233        db_table = 'auth_csrf_tokens'
     234        ordering = ('-issued',)
     235
     236    def __repr__(self):
     237        return self.token
     238
  • conf/admin_templates/delete_confirmation_generic.html

     
    22
    33{% block content %}
    44
     5{% if csrf_failed %}<p class="errornote">CSRF protection: Please re-submit.</p>{% endif %}
     6
    57{% if perms_lacking %}
    68    <p>Deleting the {{ object_name }} "{{ object }}" would result in deleting related objects, but your account doesn't have permission to delete the following types of objects:</p>
    79    <ul>
     
    1517    <form action="" method="post">
    1618    <input type="hidden" name="post" value="yes" />
    1719    <input type="submit" value="Yes, I'm sure" />
     20    {% load csrf %}{% csrf_token %}
    1821    </form>
    1922{% endif %}
    2023
  • templatetags/csrf.py

     
     1from django.core import template
     2from django.models.auth import csrf
     3import random, md5, datetime
     4
     5class CsrfTokenNode(template.Node):
     6    def render(self, context):
     7        token = md5.new(str(random.random())).hexdigest()
     8        ct = csrf.CsrfToken(None, token,
     9            datetime.datetime.now(), context['user'].id)
     10        ct.save()
     11        return '<input type="hidden" name="csrf_token" value="%s">' % token
     12
     13def csrf_token(parser, token):
     14    """
     15    {% csrf_token %}
     16    """
     17    return CsrfTokenNode()
     18
     19template.register_tag('csrf_token', csrf_token)
  • views/admin/main.py

     
    588588    if opts.admin.save_on_top:
    589589        t.extend(_get_submit_row_template(opts, app_label, add, change, show_delete, ordered_objects))
    590590    t.append('{% if form.error_dict %}<p class="errornote">Please correct the error{{ form.error_dict.items|pluralize }} below.</p>{% endif %}\n')
     591    t.append('{% if csrf_failed %}<p class="errornote">CSRF protection: Please re-submit.</p>{% endif %}\n')
    591592    for fieldset_name, options in admin_field_objs:
    592593        t.append('<fieldset class="module aligned %s">\n\n' % options.get('classes', ''))
    593594        if fieldset_name:
     
    687688        t.append('<li id="p{%% firstof %(x)s %%}"><span id="handlep{%% firstof %(x)s %%}">{{ object|truncatewords:"5" }}</span></li>' % \
    688689            {'x': ' '.join(['object.%s' % o.pk.name for o in ordered_objects])})
    689690        t.append('{% endfor %}</ul>{% endif %}\n')
     691    t.append('{% load csrf %}{% csrf_token %}\n')
    690692    t.append('</form>\n</div>\n{% endblock %}')
    691693    return ''.join(t)
    692694
     
    759761        t.append('{% endif %}')
    760762    return ''.join(t)
    761763
     764def csrf_token_check(request):
     765    from django.models.auth import csrf
     766    try:
     767        token = csrf.get_object(token__exact = request.POST.get('csrf_token', ''),
     768            select = {'user_id': request.user.id})
     769    except csrf.CsrfTokenDoesNotExist:
     770        return False
     771    token.delete()
     772    return True
     773
    762774def add_stage(request, app_label, module_name, show_delete=False, form_url='', post_url='../', post_url_continue='../%s/', object_id_override=None):
    763775    mod, opts = _get_mod_opts(app_label, module_name)
    764776    if not request.user.has_perm(app_label + '.' + opts.get_add_permission()):
    765777        raise PermissionDenied
    766778    manipulator = mod.AddManipulator()
     779    csrf_failed = False
    767780    if request.POST:
    768781        new_data = request.POST.copy()
    769782        if opts.has_field_type(meta.FileField):
    770783            new_data.update(request.FILES)
    771784        errors = manipulator.get_validation_errors(new_data)
    772         if not errors and not request.POST.has_key("_preview"):
     785        # Check the csrf_token
     786        csrf_failed = not csrf_token_check(request)
     787        if not errors and not csrf_failed and not request.POST.has_key("_preview"):
    773788            for f in opts.many_to_many:
    774789                if f.rel.raw_id_admin:
    775790                    new_data.setlist(f.name, new_data[f.name].split(","))
     
    844859        'title': 'Add %s' % opts.verbose_name,
    845860        "form": form,
    846861        "is_popup": request.REQUEST.has_key("_popup"),
     862        "csrf_failed": csrf_failed,
    847863    })
    848864    if object_id_override is not None:
    849865        c['object_id'] = object_id_override
     
    864880        raise Http404
    865881
    866882    inline_related_objects = opts.get_inline_related_objects()
     883    csrf_failed = False
    867884    if request.POST:
    868885        new_data = request.POST.copy()
    869886        if opts.has_field_type(meta.FileField):
    870887            new_data.update(request.FILES)
    871888
    872889        errors = manipulator.get_validation_errors(new_data)
    873         if not errors and not request.POST.has_key("_preview"):
     890        # Check the csrf_token
     891        csrf_failed = not csrf_token_check(request)
     892        if not errors and not csrf_failed and not request.POST.has_key("_preview"):
    874893            for f in opts.many_to_many:
    875894                if f.rel.raw_id_admin:
    876895                    new_data.setlist(f.name, new_data[f.name].split(","))
     
    969988        "form": form,
    970989        'object_id': object_id,
    971990        'original': manipulator.original_object,
    972         'is_popup' : request.REQUEST.has_key('_popup'),
     991        'is_popup': request.REQUEST.has_key('_popup'),
     992        'csrf_failed': csrf_failed,
    973993    })
    974994    raw_template = _get_template(opts, app_label, change=True)
    975995#     return HttpResponse(raw_template, mimetype='text/plain')
     
    10731093    deleted_objects = ['%s: <a href="../../%s/">%s</a>' % (capfirst(opts.verbose_name), object_id, strip_tags(repr(obj))), []]
    10741094    perms_needed = sets.Set()
    10751095    _get_deleted_objects(deleted_objects, perms_needed, request.user, obj, opts, 1)
    1076 
    1077     if request.POST: # The user has already confirmed the deletion.
     1096    csrf_failed = False
     1097    if request.POST:
     1098        csrf_failed = not csrf_token_check(request)
     1099    if request.POST and not csrf_failed: # The user has already confirmed the deletion.
    10781100        if perms_needed:
    10791101            raise PermissionDenied
    10801102        obj_repr = repr(obj)
     
    10891111        "object": obj,
    10901112        "deleted_objects": deleted_objects,
    10911113        "perms_lacking": perms_needed,
     1114        "csrf_failed": csrf_failed,
    10921115    })
    10931116    return HttpResponse(t.render(c), mimetype='text/html; charset=utf-8')
    10941117
Back to Top