Skip to content

Commit

Permalink
Merge pull request #264 from lepture/csrf
Browse files Browse the repository at this point in the history
CSRF cleanup and fixes
  • Loading branch information
davidism committed Oct 23, 2016
2 parents f306c36 + 6070ca4 commit 33b5de6
Show file tree
Hide file tree
Showing 10 changed files with 343 additions and 320 deletions.
5 changes: 3 additions & 2 deletions docs/api.rst
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
Developer Interface
===================

This part of the documentation covers all interfaces of Flask-WTF.

Forms and Fields
----------------

Expand Down Expand Up @@ -36,6 +34,9 @@ CSRF Protection
.. autoclass:: CsrfProtect
:members:

.. autoclass:: CsrfError
:members:

.. autofunction:: generate_csrf

.. autofunction:: validate_csrf
30 changes: 30 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,36 @@
Flask-WTF Changelog
===================

Version 0.14
------------

In development

- Use itsdangerous to sign CSRF tokens and check expiration instead of doing it
ourselves. (`#264`_)

- All tokens are URL safe, removing the ``url_safe`` parameter from
``generate_csrf``. (`#206`_)
- All tokens store a timestamp, which is checked in ``validate_csrf``. The
``time_limit`` parameter of ``generate_csrf`` is removed.

- Remove the ``app`` attribute from ``CsrfProtect``, use ``current_app``.
(`#264`_)
- ``CsrfProtect`` protects the ``DELETE`` method by default. (`#264`_)
- The same CSRF token is generated for the lifetime of a request. It is exposed
as ``request.csrf_token`` for use during testing. (`#227`_, `#264`_)
- ``CsrfProtect.error_handler`` is deprecated. (`#264`_)
- Handlers that return a response work in addition to those that raise an
error. The behavior was not clear in previous docs.
- (`#200`_, `#209`_, `#243`_, `#252`_)

.. _`#200`: https://github.com/lepture/flask-wtf/issues/200
.. _`#209`: https://github.com/lepture/flask-wtf/pull/209
.. _`#227`: https://github.com/lepture/flask-wtf/issues/227
.. _`#243`: https://github.com/lepture/flask-wtf/pull/243
.. _`#252`: https://github.com/lepture/flask-wtf/pull/252
.. _`#264`: https://github.com/lepture/flask-wtf/pull/264

Version 0.13.1
--------------

Expand Down
148 changes: 66 additions & 82 deletions docs/csrf.rst
Original file line number Diff line number Diff line change
@@ -1,34 +1,26 @@
CSRF Protection
===============

This part of the documentation covers the CSRF protection.

Why CSRF
--------
.. module:: flask_wtf.csrf

Flask-WTF form is already protecting you from CSRF, you don't have to
worry about that. However, you have views that contain no forms, and they
still need protection.
.. _csrf:

For example, the POST request is sent by AJAX, but it has no form behind
it. You can't get the csrf token prior 0.9.0 of Flask-WTF. That's why we
created this CSRF for you.
CSRF Protection
===============

Implementation
--------------
Any view using :class:`flask_wtf.FlaskForm` to process the request is already
getting CSRF protection. If you have views that don't use ``FlaskForm`` or make
AJAX requests, use the provided CSRF extension to protect those requests as
well.

.. module:: flask_wtf.csrf
Setup
-----

To enable CSRF protection for all your view handlers, you need to enable
the :class:`CsrfProtect` module::
To enable CSRF protection globally for a Flask app, register the
:class:`CsrfProtect` extension. ::

from flask_wtf.csrf import CsrfProtect

CsrfProtect(app)
csrf = CsrfProtect(app)

Like any other Flask extensions, you can load it lazily::

from flask_wtf.csrf import CsrfProtect
Like other Flask extensions, you can apply it lazily::

csrf = CsrfProtect()

Expand All @@ -38,94 +30,86 @@ Like any other Flask extensions, you can load it lazily::

.. note::

You need to setup a secret key for CSRF protection. Usually, this
is the same as your Flask app SECRET_KEY.
CSRF protection requires a secret key to securely sign the token. By default
this will use the Flask app's ``SECRET_KEY``. If you'd like to use a
separate token you can set ``WTF_CSRF_SECRET_KEY``.

HTML Forms
----------

If the template has a form, you don't need to do any thing. It is the
same as before:
When using a ``FlaskForm``, render the form's CSRF field like normal.

.. sourcecode:: html+jinja

<form method="post" action="/">
<form method="post">
{{ form.csrf_token }}
</form>

But if the template has no forms, you still need a csrf token:
If the template doesn't use a ``FlaskForm``, render a hidden input with the
token in the form.

.. sourcecode:: html+jinja

<form method="post" action="/">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}" />
<form method="post">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
</form>

Whenever a CSRF validation fails, it will return a 400 response. You can
customize the error response::
JavaScript Requests
-------------------

When sending an AJAX request, add the ``X-CSRFToken`` header to it.
For example, in jQuery you can configure all requests to send the token.

.. sourcecode:: html+jinja

<script type="text/javascript">
var csrf_token = "{{ csrf_token() }}";

$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrf_token);
}
}
});
</script>

Customize the error response
----------------------------

When CSRF validation fails, it will raise a :class:`CsrfError`.
By default this returns a response with the failure reason and a 400 code.
You can customize the error response using Flask's
:meth:`~flask.Flask.errorhandler`. ::

from flask_wtf.csrf import CsrfError

@csrf.error_handler
def csrf_error(reason):
return render_template('csrf_error.html', reason=reason), 400
@app.errorhandler(CsrfError)
def handle_csrf_error(e):
return render_template('csrf_error.html', reason=e.description), 400

Exclude views from protection
-----------------------------

We strongly suggest that you protect all your views with CSRF. But if
needed, you can exclude some views using a decorator::
needed, you can exclude some views using a decorator. ::

@csrf.exempt
@app.route('/foo', methods=('GET', 'POST'))
@csrf.exempt
def my_handler():
# ...
return 'ok'

If needed, you can also exclude all the views from within a given Blueprint:
You can exclude all the views of a blueprint. ::

csrf.exempt(account_blueprint)

You can also disable CSRF protection in all views by default, by setting
You can disable CSRF protection in all views by default, by setting
``WTF_CSRF_CHECK_DEFAULT`` to ``False``, and selectively call
``csrf.protect()`` only when you need. This also enables you to do some
pre-processing on the requests before checking for the CSRF token::
pre-processing on the requests before checking for the CSRF token. ::

@app.before_request
def check_csrf():
if not is_oauth(request):
csrf.protect()

AJAX
----

Sending POST requests via AJAX is possible where there are no forms at all.
This feature is available since 0.9.0.

Assuming you have done ``CsrfProtect(app)``, you can get the csrf token via
``{{ csrf_token() }}``. This method is available in every template, that
way you don't have to worry if there are no forms for rendering the csrf token
field.

The suggested way is that you render the token in a ``<meta>`` tag:

.. sourcecode:: html+jinja

<meta name="csrf-token" content="{{ csrf_token() }}">

And it is also possible to render it in the ``<script>`` tag:

.. sourcecode:: html+jinja

<script type="text/javascript">
var csrftoken = "{{ csrf_token() }}"
</script>

We will take the ``<meta>`` way for example, the ``<script>`` way is far
more easier, you don't have to worry if there is no example for it.

Whenever you send a AJAX POST request, add the ``X-CSRFToken`` for it:

.. sourcecode:: javascript

var csrftoken = $('meta[name=csrf-token]').attr('content')

$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrftoken)
}
}
})
2 changes: 2 additions & 0 deletions flask_wtf/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
if not PY2:
text_type = str
string_types = (str,)
from urllib.parse import urlparse
else:
text_type = unicode
string_types = (str, unicode)
from urlparse import urlparse


def to_bytes(text):
Expand Down
Loading

0 comments on commit 33b5de6

Please sign in to comment.