{ "info": { "author": "Simon Willison", "author_email": "simon+safeform@simonwillison.net", "bugtrack_url": null, "classifiers": [ "Development Status :: 4 - Beta", "Environment :: Web Environment", "Framework :: Django", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP" ], "description": "django_safeform\n===============\n\nCSRF protection for Django implemented at the form level - no middleware \nrequired.\n\nThere are two steps to protecting a django.forms form:\n\n1. Wrap it with the SafeForm class decorator. This adds a hidden csrf_token \n field to it along with validation logic for checking if that token has \n the correct value. It also changes the signature of the form class \n slightly, see example below.\n2. Apply the @csrf_protect middleware to the view containing the form. This \n ensures that a _csrf_cookie is correctly set.\n\nRun \"./manage.py runserver\" in the examples folder to start a Django server \ndemonstrating the functionality of the library. Use \"./manage.py test\" in the \nsame directory to run the unit tests.\n\nExample usage::\n\n from django import forms\n from django.http import HttpResponse\n from django.shortcuts import render_to_response\n from django_safeform import SafeForm, csrf_protect\n \n class ChangePasswordForm(forms.Form):\n password = forms.CharField(widget = forms.PasswordInput)\n password2 = forms.CharField(widget = forms.PasswordInput)\n ChangePasswordForm = SafeForm(ChangePasswordForm)\n \n @csrf_protect\n def change_password(request):\n form = ChangePasswordForm(request) # A\n if request.method == 'POST':\n form = ChangePasswordForm(request, request.POST)\n if form.is_valid():\n # ... change the user's password here\n return HttpResponse('Thank you')\n return render_to_response('change_password.html', {\n 'form': form,\n })\n\nNote that the form constructor signature has changed - we now pass the request\nobject as the first argument.\n\nRevision history\n----------------\n\nv2.0.0 - 18 September 2009\n\nBreaks backward compatibility with previous release - no longer changes the \nform constructor signature to take just the request object and decide whether \nor not to bind the form based on the request method. You now need to make that \ndecision yourself in your view code (just as you do with regular Django \nforms). All examples and tests have been updated.\n\nAdded CsrfTestCase to test_utils, to simplify testing of CSRF protected forms.\n\nv1.0.1 - 17 September 2009\n\nDocumentation fixes.\n\nv1.0.0 - 17 September 2009\n\nInitial release.\n\nInstallation\n------------\n\ndjango-safeform is in PyPI: http://pypi.python.org/pypi/django-safeform ::\n\n pip install django-safeform\n - OR -\n easy_install django-safeform\n\nCustom form templates\n---------------------\n\nIf your template uses one of the form rendering helper methods such as \n{{ form.as_p }} the hidden csrf_token field will be output automatically. If \nyou are rendering the form using a custom template you will need to remember \nto output that field in your template explicitly. Here's an example::\n\n
\n {{ form.non_field_errors }}\n \n \n {{ form.password }}\n \n \n \n {{ form.password2 }}\n \n
{{ form.csrf_token }}
\n \n\nNote the {{ form.csrf_token }} replacement variable just before the submit \nbutton - this will output a hidden form field containing the correct value.\n\nYou should also be sure to include {{ form.non_field_errors }} somewhere in \nyour template - this is where the \"Form session expired - please resubmit\" \nmessage will be displayed should the CSRF check fail for some reason.\n\nProtecting forms that do not use django.forms\n---------------------------------------------\n\nIf you are not using the django.forms framework - for example you are writing \nforms with hand-written HTML and pulling submitted data directly from \nrequest.POST - you can still add CSRF protection to your forms using the \n@csrf_protect decorator in conjunction with the csrf_utils module::\n\n from django_safeform import csrf_protect, csrf_utils\n \n @csrf_protect\n def hand_rolled(request):\n if request.method == 'POST':\n csrf_token = request.POST.get('csrf_token', '')\n if not csrf_utils.validate_csrf_token(csrf_token, request):\n return HttpResponse('Invalid CSRF token')\n else:\n return HttpResponse('OK')\n else:\n return HttpResponse(\"\"\"\n
\n \n \n
\n \"\"\" % csrf_utils.new_csrf_token(request))\n\nIt is your responsibility to include a hidden form field with the value from \ncsrf_utils.new_csrf_token(request) in your form, and to check that token when \nthe form is submitted using csrf_utils.validate_csrf_token.\n\nYou could also use CsrfForm to protect hand-written forms, as explained in \nthe next section.\n\nProtecting formsets / multiple forms on the same page\n-----------------------------------------------------\n\nIf you have multiple forms on the page and they are each contained in separate\n
elements, you should ensure each one has a csrf_token field, most \nlikely by configuring each one using the SafeForm class decorator.\n\nIf you have multiple django.forms forms within a single element (for \nexample, if you are using formsets) you still only need to include a single \ncsrf_token field for the overall form. In this case, rather than applying the \nSafeForm decorator to each of the form classes, it makes more sense to have a \nsingle standalone SafeForm instance within the overall form. The CsrfForm \nclass is designed to handle this exact use-case. Here's how to use it::\n \n from django import forms\n from django.forms.formsets import formset_factory\n from django_safeform import csrf_protect, CsrfForm\n from django.http import HttpResponse\n \n class PersonForm(forms.Form):\n name = forms.CharField(max_length = 100)\n email = forms.EmailField()\n \n PersonFormSet = formset_factory(PersonForm, extra=3)\n \n @csrf_protect\n def formset(request):\n csrf_form = CsrfForm(request)\n formset = PersonFormSet()\n if request.method == 'POST':\n csrf_form = CsrfForm(request, request.POST)\n formset = PersonFormSet(request.POST)\n if csrf_form.is_valid() and formset.is_valid():\n return HttpResponse('Valid: %s' % ', '.join([\n '%(name)s [%(email)s]' % form.cleaned_data \n for form in formset.forms\n if form.cleaned_data\n ]))\n return render_to_response('formset.html', {\n 'csrf_form': csrf_form,\n 'formset': formset,\n })\n\nThe formset is used in the same way as usual. The CSRF protection is \nimplemented entirely by the separate CsrfForm instance. In the template, the \nCsrfForm should be output using {{ csrf_form }} like this::\n\n \n {{ csrf_form }}\n {% for form in formset.forms %}\n {{ form.as_p }}\n {% endfor %}\n

\n {{ formset.management_form }}\n \n

\n
\n\nThe {{ csrf_form }} template tag specifies where the hidden input field \ncontaining the form token should be included. Should a CSRF failure occur, it \nalso specifies where the
    containing the CSRF failure \nmessage should be displayed.\n\nIf you want to include the hidden input field in a different location to the \nerror message, you can use {{ csrf_form.csrf_token }} to output the hidden \nfield and {{ csrf_form.non_field_errors }} to output the error message::\n\n
    \n {{ csrf_form.non_field_errors }}\n {% for form in formset.forms %}\n {{ form.as_p }}\n {% endfor %}\n

    \n {{ formset.management_form }}\n {{ csrf_form.csrf_token }}\n \n

    \n
    \n\n\nChanging the CSRF error message\n-------------------------------\n\nThe default message shown to the user if the CSRF check fails is::\n\n Form session expired - please resubmit\n\nThe wording here is deliberately a bit vague - most users will have no idea \nwhat a \"CSRF failure\" is, but users have probably seen \"session expired\" \nmessages before. A \"form session\" seems like a reasonable metaphor for what \nis going on under the hood.\n\nIf you dislike this message, you can over-ride it in your call to the \nSafeForm class decorator::\n\n ChangePasswordForm = SafeForm(ChangePasswordForm,\n invalid_message='CSRF check failed'\n )\n\nHandling Ajax\n-------------\n\nBy default, Ajax requests do NOT have CSRF protection applied to them - they \nwill be ignored by the form validator, which looks out for any requests where \nrequest.is_ajax() returns True (i.e. requests which have a X-Requested_With\nheader set to XMLHttpRequest). This custom header is set by most common Ajax \nlibraries. Protection is not needed here because it is not possible to forge \nHTTP headers when committing a CSRF attack using an HTML form.\n\nIf you are ultra-paranoid and want to apply CSRF protection even to requests \nwith that header, you can disable the Ajax special case like this::\n\n ChangePasswordForm = SafeForm(ChangePasswordForm,\n ajax_skips_check=False\n )\n\nIf you do this, you will need to ensure the csrf_token is included in Ajax\nPOST requests yourself. One way to do this would be to read the token out of \nthe first hidden input field with name=\"csrf_token\" using the JavaScript DOM.\n\nEnhancing security\n------------------\n\nBy default, the form tokens served up in hidden fields are valid for POST \nsubmissions to any form on the site (for the user with that cookie) and never \nexpire. You can limit the scope of the tokens in two ways - by tying them to \na specific form, or by causing them to expire after a number of seconds.\n\nTo tie a token to one specific form, simply pass an identifier argument to \nthe SafeForm decorator::\n\n ChangePasswordForm = SafeForm(ChangePasswordForm,\n identifier='change-password'\n )\n\nTokens generated for that form will now only allow submissions back to the \nsame form. If you are using the csrf_utils module directly (in a hand-rolled \nform for example) you should pass identifier arguments to both the \nnew_csrf_token and validate_csrf_token functions::\n\n token = csrf_utils.new_csrf_token(request, identifier='my-custom-form')\n # ... later ...\n token_ok = csrf_utils.validate_csrf_token(\n token, request, identifier='my-custom-form'\n )\n if not token_ok:\n return HttpResponse('Invalid CSRF token')\n\nTo cause your form tokens to expire, use the expire_after argument in your \ncall to the SafeForm class decorator::\n\n ChangePasswordForm = SafeForm(ChangePasswordForm,\n identifier='change-password',\n expire_after=24 * 60 * 60 # Expire after 24 hours\n )\n\nIf using csrf_utils directly, pass that argument to the validate_csrf_token \nfunction::\n\n token_ok = csrf_utils.validate_csrf_token(\n token, request, identifier='my-custom-form', expire_after=24 * 60 * 60\n )\n\nThe default expire_after value is set by the CSRF_TOKENS_EXPIRE_AFTER setting,\nwhich defaults to None. If you want all of your CSRF tokens to expire, add \nthis to your settings.py file::\n\n CSRF_TOKENS_EXPIRE_AFTER = 24 * 60 * 60\n\nWhen a token expires, the user will see the CSRF error message described above\nbut will not lose their form submission, so don't worry too much about the \nconsequences of setting a strict timeout.\n\nProtecting GET forms\n--------------------\n\nThe examples so far have all been for forms submitted using the POST verb. It \nis also possible to protect GET forms against CSRF, but you should very rarely\nneed to do this. HTTP specifies that actions submitted via GET should be \nidempotent, which is generally interpreted as ruling that they should not \ncause state changes to your system. As such, an authenticated GET request \nshould not be able to cause any damage.\n\nMany GET forms are specifically designed to allow links from other sites to \ntrigger an action - search forms for example. If your application has GET \nrequests that require CSRF protection you should probably rethink the design \nof your application.\n\nIf you decide you do need to add CSRF protection to a GET form, you should be \naware that it is much easier for csrf_tokens used in GET requests to \"leak\"\nto an external attacker. URLs show up in Browser referral headers, so links \nto external sites from your CSRF protected GET page will inadvertently pass \nthe token on to those sites. Your users may also accidentally share their \nCSRF tokens by pasting them in to e-mails or bookmarking them on link sharing \nsites.\n\nConsequently, any CSRF tokens used in GET forms should take advantage of the \nextra security features documented above - they should use an identifier to \nlock the token down just to that form and should specify a strict expiry time \nto limit the damage that can be caused should the token accidentally leak.\n\nUnit testing\n------------\n\nProperly unit testing CSRF protection is significantly more complicated than \na regular unit test, as you need to first GET the initial form, then extract \nthe csrf_token field from it, then submit that as part of the POST. The unit \ntests that ship with django_safeform show how to do this.\n\nYou can shortcut this process by using CsrfTestCase as the base class for your\nunit tests. This swaps in an alternative Client implementation which causes \nPOST requests using client.post() to automatically include a valid CSRF token.\n\nHere's an example test using CsrfTestCase::\n\n from django_safeform import test_utils\n \n class SubmitTestCase(test_utils.CsrfTestCase):\n def test_submission_with_correct_csrf_token_works(self):\n response = self.client.post('/safe-basic-form/', {\n 'name': 'Test',\n })\n self.assertEqual(response.content, 'Valid: Test')\n\nIf you are using the CsrfTestCase subclass but you do NOT wish to include the \ncsrf_token automatically in one of your tests, pass a csrf=False argument to \nthe client.post() method::\n\n def test_submission_without_csrf_token(self):\n response = self.client.post('/safe-basic-form/', {\n 'name': 'Test',\n }, csrf=False)\n self.assert_(CSRF_INVALID_MESSAGE in response.content)\n\nIf you are protecting your forms with a custom form identifier, you should \npass that identifier as the csrf argument::\n\n def test_change_password(self):\n response = self.client.post('/change-password/', {\n 'password': 'new-password',\n 'password2': 'new-password',\n }, csrf='change-password')\n # ...\n\nIf you are already using your own custom TestCase subclass and do not wish to \nuse CsrfTestCase, you can instantiate the special client in your own setUp\nmethod::\n \n from django_safeform import test_utils\n from django.test import TestCase\n \n class MyTestCaseSubclass(TestCase):\n def setUp(self):\n self.client = test_utils.CsrfClient()\n\nDesign notes\n------------\n\nApps shipped with Django, in particular the admin, MUST be secure against CSRF \nno matter what the user's configuration is (so dependency on middleware alone \nis a problem).\n\nSecure by default for user code would be nice, but in its absence explicitly \nraising developer awareness of CSRF is probably a good thing.\n\nShould not be tied to sessions - some developers might not be using them.\n\nShould not require the form framework - hand-rolled forms should be easy to \nprotect too.\n\nThe original idea was to have an alternative Form base class called SafeForm - \nthis was replaced with a class decorator when we realised that we would \notherwise also need to provide SafeModelForm, SafeFormSet and so on.\n\nCredits\n-------\n\nThis library was developed from a discussion with Russell Keith-Magee, \nAndrew Godwin and Armin Ronacher at the DjangoCon 2009 sprints, and improved \nbased on extensive feedback from Luke Plant on the django-developers mailing \nlist.\n\nTODO\n----\n\n - Figure out what to do about protecting forms which already alter the \n Form constructor signature themselves.\n - _csrf_token_from_request should throw error if cookie has not been set by \n the csrf_protect decorator.\n - Improved support for unit testing CSRF protected forms.\n\nAlternative approaches\n----------------------\n\nPure middleware:\n - breaks with etags\n - rewrites HTML\n - doesn't work with streaming\n - you have to decorate things as exempt\n - potential leakage of external forms\n - XHTML v.s. HTML\n\nMiddleware and template tags and RequestContext:\n - uses a view middleware\n - applying by default is error prone\n - if user disables middleware, admin becomes insecure\n - requires RequestContext\n - form submissions are lost on CSRF failure", "description_content_type": null, "docs_url": null, "download_url": "UNKNOWN", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "http://github.com/simonw/django-safeform", "keywords": null, "license": "BSD", "maintainer": null, "maintainer_email": null, "name": "django-safeform", "package_url": "https://pypi.org/project/django-safeform/", "platform": "Any", "project_url": "https://pypi.org/project/django-safeform/", "project_urls": { "Download": "UNKNOWN", "Homepage": "http://github.com/simonw/django-safeform" }, "release_url": "https://pypi.org/project/django-safeform/2.0.0/", "requires_dist": null, "requires_python": null, "summary": "CSRF protection for Django forms", "version": "2.0.0" }, "last_serial": 790532, "releases": { "1.0": [ { "comment_text": "", "digests": { "md5": "65b86a95975dc0c1e76b61d948253387", "sha256": "f84b7380d9138ab6f3f2970e7bd104542184ed39fe7fd9453185dd012895b216" }, "downloads": -1, "filename": "django-safeform-1.0.tar.gz", "has_sig": false, "md5_digest": "65b86a95975dc0c1e76b61d948253387", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 10017, "upload_time": "2009-09-17T08:57:42", "url": "https://files.pythonhosted.org/packages/7a/28/9a8ad181e0a46dfc3d21ea75c44c50353eba112259f13289248c15ec712a/django-safeform-1.0.tar.gz" } ], "1.0.1": [ { "comment_text": "", "digests": { "md5": "8a28595f907c6d1a36b0f14d81025046", "sha256": "9cab90ab0c7f09ee84abb339dd772990a995cff383caeb7149749803e62c6cf8" }, "downloads": -1, "filename": "django-safeform-1.0.1.tar.gz", "has_sig": false, "md5_digest": "8a28595f907c6d1a36b0f14d81025046", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 10017, "upload_time": "2009-09-17T09:21:23", "url": "https://files.pythonhosted.org/packages/da/b7/eabf60876b3e9cacad1c319516ebde602f8084ab56bf676ad93131166e48/django-safeform-1.0.1.tar.gz" } ], "2.0.0": [ { "comment_text": "", "digests": { "md5": "6c5f0cfe05bb1e5b3c1dc923686d686f", "sha256": "428e9ab6b018911ea499e13798d1a2e24832ddc6144bd21e911ed5c3a9864151" }, "downloads": -1, "filename": "django-safeform-2.0.0.tar.gz", "has_sig": false, "md5_digest": "6c5f0cfe05bb1e5b3c1dc923686d686f", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 12471, "upload_time": "2009-09-18T23:47:41", "url": "https://files.pythonhosted.org/packages/96/8e/a1569ae6c1aa09fd7b80d783293537c366c324169cf03df4d8fc2886bb03/django-safeform-2.0.0.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "6c5f0cfe05bb1e5b3c1dc923686d686f", "sha256": "428e9ab6b018911ea499e13798d1a2e24832ddc6144bd21e911ed5c3a9864151" }, "downloads": -1, "filename": "django-safeform-2.0.0.tar.gz", "has_sig": false, "md5_digest": "6c5f0cfe05bb1e5b3c1dc923686d686f", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 12471, "upload_time": "2009-09-18T23:47:41", "url": "https://files.pythonhosted.org/packages/96/8e/a1569ae6c1aa09fd7b80d783293537c366c324169cf03df4d8fc2886bb03/django-safeform-2.0.0.tar.gz" } ] }