Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Increase the protection provided by the CSRF checks #2500

Merged
merged 2 commits into from
Apr 15, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions docs/narr/sessions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -391,8 +391,8 @@ will return ``True``, otherwise it will raise ``HTTPBadRequest``. Optionally,
you can specify ``raises=False`` to have the check return ``False`` instead of
raising an exception.

By default, it checks for a GET or POST parameter named ``csrf_token`` or a
header named ``X-CSRF-Token``.
By default, it checks for a POST parameter named ``csrf_token`` or a header
named ``X-CSRF-Token``.

.. code-block:: python

Expand All @@ -411,15 +411,16 @@ Checking CSRF Tokens Automatically

.. versionadded:: 1.7

:app:`Pyramid` supports automatically checking CSRF tokens on POST requests.
Any other request may be checked manually. This feature can be turned on
globally for an application using the ``pyramid.require_default_csrf`` setting.
:app:`Pyramid` supports automatically checking CSRF tokens on requests with an
unsafe method as defined by RFC2616. Any other request may be checked manually.
This feature can be turned on globally for an application using the
``pyramid.require_default_csrf`` setting.

If the ``pyramid.required_default_csrf`` setting is a :term:`truthy string` or
``True`` then the default CSRF token parameter will be ``csrf_token``. If a
different token is desired, it may be passed as the value. Finally, a
:term:`falsey string` or ``False`` will turn off automatic CSRF checking
globally on every POST request.
globally on every request.

No matter what, CSRF checking may be explicitly enabled or disabled on a
per-view basis using the ``require_csrf`` view option. This option is of the
Expand All @@ -430,8 +431,7 @@ If ``require_csrf`` is ``True`` but does not explicitly define a token to
check, then the token name is pulled from whatever was set in the
``pyramid.require_default_csrf`` setting. Finally, if that setting does not
explicitly define a token, then ``csrf_token`` is the token required. This token
name will be required in ``request.params`` which is a combination of the
query string and a submitted form body.
name will be required in ``request.POST`` which is the submitted form body.

It is always possible to pass the token in the ``X-CSRF-Token`` header as well.
There is currently no way to define an alternate name for this header without
Expand Down
11 changes: 6 additions & 5 deletions docs/narr/viewconfig.rst
Original file line number Diff line number Diff line change
Expand Up @@ -195,10 +195,11 @@ Non-Predicate Arguments

``require_csrf``

CSRF checks only affect POST requests. Any other request methods will pass
untouched. This option is used in combination with the
``pyramid.require_default_csrf`` setting to control which request parameters
are checked for CSRF tokens.
CSRF checks will affect any request method that is not defined as a "safe"
method by RFC2616. In pratice this means that GET, HEAD, OPTIONS, and TRACE
methods will pass untouched and all others methods will require CSRF. This
option is used in combination with the ``pyramid.require_default_csrf``
setting to control which request parameters are checked for CSRF tokens.

This feature requires a configured :term:`session factory`.

Expand Down Expand Up @@ -459,7 +460,7 @@ configured view.
check name.

If CSRF checking is performed, the checked value will be the value of
``request.params[check_name]``. This value will be compared against the
``request.POST[check_name]``. This value will be compared against the
value of ``request.session.get_csrf_token()``, and the check will pass if
these two values are the same. If the check passes, the associated view will
be permitted to execute. If the check fails, the associated view will not be
Expand Down
4 changes: 2 additions & 2 deletions docs/whatsnew-1.7.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ Feature Additions
to security checks. See https://github.com/Pylons/pyramid/pull/2021

- Added a new setting, ``pyramid.require_default_csrf`` which may be used
to turn on CSRF checks globally for every POST request in the application.
to turn on CSRF checks globally for every request in the application.
This should be considered a good default for websites built on Pyramid.
It is possible to opt-out of CSRF checks on a per-view basis by setting
``require_csrf=False`` on those views.
See :ref:`auto_csrf_checking` and
https://github.com/Pylons/pyramid/pull/2413

- Added a ``require_csrf`` view option which will enforce CSRF checks on POST
- Added a ``require_csrf`` view option which will enforce CSRF checks on
requests. If the CSRF check fails a ``BadCSRFToken`` exception will be
raised and may be caught by exception views (the default response is a
``400 Bad Request``). This option should be used in place of the deprecated
Expand Down
28 changes: 20 additions & 8 deletions pyramid/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,14 @@ def check_csrf_token(request,
header='X-CSRF-Token',
raises=True):
""" Check the CSRF token in the request's session against the value in
``request.params.get(token)`` or ``request.headers.get(header)``.
If a ``token`` keyword is not supplied to this function, the string
``csrf_token`` will be used to look up the token in ``request.params``.
If a ``header`` keyword is not supplied to this function, the string
``X-CSRF-Token`` will be used to look up the token in ``request.headers``.

If the value supplied by param or by header doesn't match the value
``request.POST.get(token)`` (if a POST request) or
``request.headers.get(header)``. If a ``token`` keyword is not supplied to
this function, the string ``csrf_token`` will be used to look up the token
in ``request.POST``. If a ``header`` keyword is not supplied to this
function, the string ``X-CSRF-Token`` will be used to look up the token in
``request.headers``.

If the value supplied by post or by header doesn't match the value
supplied by ``request.session.get_csrf_token()``, and ``raises`` is
``True``, this function will raise an
:exc:`pyramid.exceptions.BadCSRFToken` exception.
Expand All @@ -128,7 +129,18 @@ def check_csrf_token(request,

.. versionadded:: 1.4a2
"""
supplied_token = request.params.get(token, request.headers.get(header, ""))
# If this is a POST/PUT/etc request, then we'll check the body to see if it
# has a token. We explicitly use request.POST here because CSRF tokens
# should never appear in an URL as doing so is a security issue. We also
# explicitly check for request.POST here as we do not support sending form
# encoded data over anything but a request.POST.
supplied_token = request.POST.get(token, "")

# If we were unable to locate a CSRF token in a request body, then we'll
# check to see if there are any headers that have a value for us.
if supplied_token == "":
supplied_token = request.headers.get(header, "")

expected_token = request.session.get_csrf_token()
if strings_differ(bytes_(expected_token), bytes_(supplied_token)):
if raises:
Expand Down
7 changes: 5 additions & 2 deletions pyramid/tests/test_config/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -1502,8 +1502,9 @@ def test_add_view_with_check_csrf_predicates_match(self):
self.assertEqual(len(w), 1)
wrapper = self._getViewCallable(config)
request = self._makeRequest(config)
request.method = "POST"
request.session = DummySession({'csrf_token': 'foo'})
request.params = {'csrf_token': 'foo'}
request.POST = {'csrf_token': 'foo'}
request.headers = {}
self.assertEqual(wrapper(None, request), 'OK')

Expand Down Expand Up @@ -1595,7 +1596,7 @@ def view(request):
view = self._getViewCallable(config)
request = self._makeRequest(config)
request.method = 'POST'
request.params = {'st': 'foo'}
request.POST = {'st': 'foo'}
request.headers = {}
request.session = DummySession({'csrf_token': 'foo'})
self.assertEqual(view(None, request), 'OK')
Expand All @@ -1609,6 +1610,7 @@ def view(request):
view = self._getViewCallable(config)
request = self._makeRequest(config)
request.method = 'POST'
request.POST = {}
request.headers = {'X-CSRF-Token': 'foo'}
request.session = DummySession({'csrf_token': 'foo'})
self.assertEqual(view(None, request), 'OK')
Expand All @@ -1622,6 +1624,7 @@ def view(request): return 'OK'
view = self._getViewCallable(config)
request = self._makeRequest(config)
request.method = 'POST'
request.POST = {}
request.headers = {}
request.session = DummySession({'csrf_token': 'foo'})
self.assertRaises(BadCSRFToken, lambda: view(None, request))
Expand Down
9 changes: 6 additions & 3 deletions pyramid/tests/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -666,7 +666,8 @@ def _callFUT(self, *args, **kwargs):

def test_success_token(self):
request = testing.DummyRequest()
request.params['csrf_token'] = request.session.get_csrf_token()
request.method = "POST"
request.POST = {'csrf_token': request.session.get_csrf_token()}
self.assertEqual(self._callFUT(request, token='csrf_token'), True)

def test_success_header(self):
Expand All @@ -676,7 +677,8 @@ def test_success_header(self):

def test_success_default_token(self):
request = testing.DummyRequest()
request.params['csrf_token'] = request.session.get_csrf_token()
request.method = "POST"
request.POST = {'csrf_token': request.session.get_csrf_token()}
self.assertEqual(self._callFUT(request), True)

def test_success_default_header(self):
Expand All @@ -698,8 +700,9 @@ def test_failure_no_raises(self):
def test_token_differing_types(self):
from pyramid.compat import text_
request = testing.DummyRequest()
request.method = "POST"
request.session['_csrft_'] = text_('foo')
request.params['csrf_token'] = b'foo'
request.POST = {'csrf_token': b'foo'}
self.assertEqual(self._callFUT(request, token='csrf_token'), True)

class DummySerializer(object):
Expand Down
25 changes: 19 additions & 6 deletions pyramid/tests/test_viewderivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,7 @@ def inner_view(request):
return response
request = self._makeRequest()
request.method = 'POST'
request.POST = {}
request.session = DummySession({'csrf_token': 'foo'})
request.headers = {'X-CSRF-Token': 'foo'}
view = self.config._derive_view(inner_view, require_csrf=True)
Expand All @@ -1133,7 +1134,7 @@ def inner_view(request):
request = self._makeRequest()
request.method = 'POST'
request.session = DummySession({'csrf_token': 'foo'})
request.params['DUMMY'] = 'foo'
request.POST = {'DUMMY': 'foo'}
view = self.config._derive_view(inner_view, require_csrf='DUMMY')
result = view(None, request)
self.assertTrue(result is response)
Expand All @@ -1154,7 +1155,7 @@ def inner_view(request): pass
request = self._makeRequest()
request.method = 'POST'
request.session = DummySession({'csrf_token': 'foo'})
request.params['DUMMY'] = 'bar'
request.POST = {'DUMMY': 'bar'}
view = self.config._derive_view(inner_view, require_csrf='DUMMY')
self.assertRaises(BadCSRFToken, lambda: view(None, request))

Expand All @@ -1163,6 +1164,18 @@ def test_csrf_view_fails_on_bad_POST_header(self):
def inner_view(request): pass
request = self._makeRequest()
request.method = 'POST'
request.POST = {}
request.session = DummySession({'csrf_token': 'foo'})
request.headers = {'X-CSRF-Token': 'bar'}
view = self.config._derive_view(inner_view, require_csrf='DUMMY')
self.assertRaises(BadCSRFToken, lambda: view(None, request))

def test_csrf_view_fails_on_bad_PUT_header(self):
from pyramid.exceptions import BadCSRFToken
def inner_view(request): pass
request = self._makeRequest()
request.method = 'PUT'
request.POST = {}
request.session = DummySession({'csrf_token': 'foo'})
request.headers = {'X-CSRF-Token': 'bar'}
view = self.config._derive_view(inner_view, require_csrf='DUMMY')
Expand All @@ -1175,7 +1188,7 @@ def inner_view(request):
request = self._makeRequest()
request.method = 'POST'
request.session = DummySession({'csrf_token': 'foo'})
request.params['csrf_token'] = 'foo'
request.POST = {'csrf_token': 'foo'}
self.config.add_settings({'pyramid.require_default_csrf': 'yes'})
view = self.config._derive_view(inner_view)
result = view(None, request)
Expand All @@ -1188,7 +1201,7 @@ def inner_view(request):
request = self._makeRequest()
request.method = 'POST'
request.session = DummySession({'csrf_token': 'foo'})
request.params['DUMMY'] = 'foo'
request.POST = {'DUMMY': 'foo'}
self.config.add_settings({'pyramid.require_default_csrf': 'DUMMY'})
view = self.config._derive_view(inner_view)
result = view(None, request)
Expand All @@ -1214,7 +1227,7 @@ def inner_view(request):
request = self._makeRequest()
request.method = 'POST'
request.session = DummySession({'csrf_token': 'foo'})
request.params['DUMMY'] = 'foo'
request.POST = {'DUMMY': 'foo'}
self.config.add_settings({'pyramid.require_default_csrf': 'yes'})
view = self.config._derive_view(inner_view, require_csrf='DUMMY')
result = view(None, request)
Expand All @@ -1227,7 +1240,7 @@ def inner_view(request):
request = self._makeRequest()
request.method = 'POST'
request.session = DummySession({'csrf_token': 'foo'})
request.params['DUMMY'] = 'foo'
request.POST = {'DUMMY': 'foo'}
self.config.add_settings({'pyramid.require_default_csrf': 'DUMMY'})
view = self.config._derive_view(inner_view, require_csrf=True)
result = view(None, request)
Expand Down
4 changes: 3 additions & 1 deletion pyramid/viewderivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,9 @@ def csrf_view(view, info):
wrapped_view = view
if val:
def csrf_view(context, request):
if request.method == 'POST':
# Assume that anything not defined as 'safe' by RFC2616 needs
# protection
if request.method not in {"GET", "HEAD", "OPTIONS", "TRACE"}:
check_csrf_token(request, val, raises=True)
return view(context, request)
wrapped_view = csrf_view
Expand Down