Steve Piercy
2018-09-22 2a45fe74f9598b4e726ab17ce17948d4e709894b
commit | author | age
a2c7c7 1 import uuid
MW 2
7c0f09 3 from webob.cookies import CookieProfile
a2c7c7 4 from zope.interface import implementer
7c0f09 5
MW 6
7 from pyramid.authentication import _SimpleSerializer
a2c7c7 8
MW 9 from pyramid.compat import (
682a9b 10     bytes_,
a2c7c7 11     urlparse,
682a9b 12     text_,
a2c7c7 13 )
MW 14 from pyramid.exceptions import (
15     BadCSRFOrigin,
16     BadCSRFToken,
17 )
fe0d22 18 from pyramid.interfaces import ICSRFStoragePolicy
a2c7c7 19 from pyramid.settings import aslist
MW 20 from pyramid.util import (
21     is_same_domain,
22     strings_differ
23 )
24
25
fe0d22 26 @implementer(ICSRFStoragePolicy)
682a9b 27 class LegacySessionCSRFStoragePolicy(object):
MM 28     """ A CSRF storage policy that defers control of CSRF storage to the
29     session.
30
31     This policy maintains compatibility with legacy ISession implementations
32     that know how to manage CSRF tokens themselves via
33     ``ISession.new_csrf_token`` and ``ISession.get_csrf_token``.
a2c7c7 34
MW 35     Note that using this CSRF implementation requires that
36     a :term:`session factory` is configured.
37
682a9b 38     .. versionadded:: 1.9
MM 39
a2c7c7 40     """
MW 41     def new_csrf_token(self, request):
42         """ Sets a new CSRF token into the session and returns it. """
43         return request.session.new_csrf_token()
44
45     def get_csrf_token(self, request):
682a9b 46         """ Returns the currently active CSRF token from the session,
MM 47         generating a new one if needed."""
a2c7c7 48         return request.session.get_csrf_token()
MW 49
3f14d6 50     def check_csrf_token(self, request, supplied_token):
MM 51         """ Returns ``True`` if the ``supplied_token`` is valid."""
52         expected_token = self.get_csrf_token(request)
53         return not strings_differ(
54             bytes_(expected_token), bytes_(supplied_token))
55
682a9b 56
MM 57 @implementer(ICSRFStoragePolicy)
58 class SessionCSRFStoragePolicy(object):
59     """ A CSRF storage policy that persists the CSRF token in the session.
60
61     Note that using this CSRF implementation requires that
62     a :term:`session factory` is configured.
63
64     ``key``
65
66         The session key where the CSRF token will be stored.
67         Default: `_csrft_`.
68
69     .. versionadded:: 1.9
70
71     """
72     _token_factory = staticmethod(lambda: text_(uuid.uuid4().hex))
73
74     def __init__(self, key='_csrft_'):
75         self.key = key
76
77     def new_csrf_token(self, request):
78         """ Sets a new CSRF token into the session and returns it. """
79         token = self._token_factory()
80         request.session[self.key] = token
81         return token
82
83     def get_csrf_token(self, request):
84         """ Returns the currently active CSRF token from the session,
85         generating a new one if needed."""
86         token = request.session.get(self.key, None)
87         if not token:
88             token = self.new_csrf_token(request)
89         return token
3f14d6 90
MM 91     def check_csrf_token(self, request, supplied_token):
92         """ Returns ``True`` if the ``supplied_token`` is valid."""
93         expected_token = self.get_csrf_token(request)
94         return not strings_differ(
95             bytes_(expected_token), bytes_(supplied_token))
682a9b 96
a2c7c7 97
fe0d22 98 @implementer(ICSRFStoragePolicy)
7c0f09 99 class CookieCSRFStoragePolicy(object):
a2c7c7 100     """ An alternative CSRF implementation that stores its information in
MW 101     unauthenticated cookies, known as the 'Double Submit Cookie' method in the
682a9b 102     `OWASP CSRF guidelines <https://www.owasp.org/index.php/
MM 103     Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet#
104     Double_Submit_Cookie>`_. This gives some additional flexibility with
105     regards to scaling as the tokens can be generated and verified by a
106     front-end server.
313c25 107
682a9b 108     .. versionadded:: 1.9
MM 109
a2c7c7 110     """
682a9b 111     _token_factory = staticmethod(lambda: text_(uuid.uuid4().hex))
313c25 112
a2c7c7 113     def __init__(self, cookie_name='csrf_token', secure=False, httponly=False,
7c0f09 114                  domain=None, max_age=None, path='/'):
MW 115         serializer = _SimpleSerializer()
116         self.cookie_profile = CookieProfile(
117             cookie_name=cookie_name,
118             secure=secure,
119             max_age=max_age,
120             httponly=httponly,
121             path=path,
682a9b 122             domains=[domain],
7c0f09 123             serializer=serializer
MW 124         )
682a9b 125         self.cookie_name = cookie_name
a2c7c7 126
MW 127     def new_csrf_token(self, request):
128         """ Sets a new CSRF token into the request and returns it. """
682a9b 129         token = self._token_factory()
MM 130         request.cookies[self.cookie_name] = token
a2c7c7 131         def set_cookie(request, response):
7c0f09 132             self.cookie_profile.set_cookies(
MW 133                 response,
a2c7c7 134                 token,
MW 135             )
136         request.add_response_callback(set_cookie)
137         return token
138
139     def get_csrf_token(self, request):
140         """ Returns the currently active CSRF token by checking the cookies
141         sent with the current request."""
7c0f09 142         bound_cookies = self.cookie_profile.bind(request)
MW 143         token = bound_cookies.get_value()
a2c7c7 144         if not token:
MW 145             token = self.new_csrf_token(request)
146         return token
147
3f14d6 148     def check_csrf_token(self, request, supplied_token):
MM 149         """ Returns ``True`` if the ``supplied_token`` is valid."""
150         expected_token = self.get_csrf_token(request)
151         return not strings_differ(
152             bytes_(expected_token), bytes_(supplied_token))
153
a2c7c7 154
MW 155 def get_csrf_token(request):
156     """ Get the currently active CSRF token for the request passed, generating
157     a new one using ``new_csrf_token(request)`` if one does not exist. This
158     calls the equivalent method in the chosen CSRF protection implementation.
159
2ded2f 160     .. versionadded :: 1.9
3f14d6 161
a2c7c7 162     """
MW 163     registry = request.registry
fe0d22 164     csrf = registry.getUtility(ICSRFStoragePolicy)
313c25 165     return csrf.get_csrf_token(request)
a2c7c7 166
MW 167
168 def new_csrf_token(request):
169     """ Generate a new CSRF token for the request passed and persist it in an
170     implementation defined manner. This calls the equivalent method in the
171     chosen CSRF protection implementation.
172
2ded2f 173     .. versionadded :: 1.9
3f14d6 174
a2c7c7 175     """
MW 176     registry = request.registry
fe0d22 177     csrf = registry.getUtility(ICSRFStoragePolicy)
313c25 178     return csrf.new_csrf_token(request)
a2c7c7 179
MW 180
181 def check_csrf_token(request,
182                      token='csrf_token',
183                      header='X-CSRF-Token',
184                      raises=True):
313c25 185     """ Check the CSRF token returned by the
682a9b 186     :class:`pyramid.interfaces.ICSRFStoragePolicy` implementation against the
MM 187     value in ``request.POST.get(token)`` (if a POST request) or
313c25 188     ``request.headers.get(header)``. If a ``token`` keyword is not supplied to
JC 189     this function, the string ``csrf_token`` will be used to look up the token
190     in ``request.POST``. If a ``header`` keyword is not supplied to this
191     function, the string ``X-CSRF-Token`` will be used to look up the token in
192     ``request.headers``.
a2c7c7 193
3f14d6 194     If the value supplied by post or by header cannot be verified by the
MM 195     :class:`pyramid.interfaces.ICSRFStoragePolicy`, and ``raises`` is
682a9b 196     ``True``, this function will raise an
MM 197     :exc:`pyramid.exceptions.BadCSRFToken` exception. If the values differ
198     and ``raises`` is ``False``, this function will return ``False``.  If the
199     CSRF check is successful, this function will return ``True``
200     unconditionally.
a2c7c7 201
MW 202     See :ref:`auto_csrf_checking` for information about how to secure your
203     application automatically against CSRF attacks.
204
205     .. versionadded:: 1.4a2
206
207     .. versionchanged:: 1.7a1
208        A CSRF token passed in the query string of the request is no longer
209        considered valid. It must be passed in either the request body or
210        a header.
211
2ded2f 212     .. versionchanged:: 1.9
3f14d6 213        Moved from :mod:`pyramid.session` to :mod:`pyramid.csrf` and updated
MM 214        to use the configured :class:`pyramid.interfaces.ICSRFStoragePolicy` to
215        verify the CSRF token.
216
a2c7c7 217     """
MW 218     supplied_token = ""
219     # We first check the headers for a csrf token, as that is significantly
220     # cheaper than checking the POST body
221     if header is not None:
222         supplied_token = request.headers.get(header, "")
223
224     # If this is a POST/PUT/etc request, then we'll check the body to see if it
225     # has a token. We explicitly use request.POST here because CSRF tokens
226     # should never appear in an URL as doing so is a security issue. We also
227     # explicitly check for request.POST here as we do not support sending form
228     # encoded data over anything but a request.POST.
229     if supplied_token == "" and token is not None:
230         supplied_token = request.POST.get(token, "")
231
3f14d6 232     policy = request.registry.getUtility(ICSRFStoragePolicy)
MM 233     if not policy.check_csrf_token(request, text_(supplied_token)):
a2c7c7 234         if raises:
MW 235             raise BadCSRFToken('check_csrf_token(): Invalid token')
236         return False
237     return True
238
239
240 def check_csrf_origin(request, trusted_origins=None, raises=True):
241     """
2ded2f 242     Check the ``Origin`` of the request to see if it is a cross site request or
a2c7c7 243     not.
MW 244
2ded2f 245     If the value supplied by the ``Origin`` or ``Referer`` header isn't one of the
a2c7c7 246     trusted origins and ``raises`` is ``True``, this function will raise a
2ded2f 247     :exc:`pyramid.exceptions.BadCSRFOrigin` exception, but if ``raises`` is
MW 248     ``False``, this function will return ``False`` instead. If the CSRF origin
a2c7c7 249     checks are successful this function will return ``True`` unconditionally.
MW 250
251     Additional trusted origins may be added by passing a list of domain (and
69828b 252     ports if non-standard like ``['example.com', 'dev.example.com:8080']``) in
a2c7c7 253     with the ``trusted_origins`` parameter. If ``trusted_origins`` is ``None``
MW 254     (the default) this list of additional domains will be pulled from the
255     ``pyramid.csrf_trusted_origins`` setting.
256
2ded2f 257     Note that this function will do nothing if ``request.scheme`` is not
MW 258     ``https``.
a2c7c7 259
MW 260     .. versionadded:: 1.7
261
2ded2f 262     .. versionchanged:: 1.9
MW 263        Moved from :mod:`pyramid.session` to :mod:`pyramid.csrf`
3f14d6 264
a2c7c7 265     """
MW 266     def _fail(reason):
267         if raises:
268             raise BadCSRFOrigin(reason)
269         else:
270             return False
271
272     if request.scheme == "https":
273         # Suppose user visits http://example.com/
274         # An active network attacker (man-in-the-middle, MITM) sends a
275         # POST form that targets https://example.com/detonate-bomb/ and
276         # submits it via JavaScript.
277         #
278         # The attacker will need to provide a CSRF cookie and token, but
279         # that's no problem for a MITM when we cannot make any assumptions
280         # about what kind of session storage is being used. So the MITM can
281         # circumvent the CSRF protection. This is true for any HTTP connection,
282         # but anyone using HTTPS expects better! For this reason, for
283         # https://example.com/ we need additional protection that treats
284         # http://example.com/ as completely untrusted. Under HTTPS,
285         # Barth et al. found that the Referer header is missing for
286         # same-domain requests in only about 0.2% of cases or less, so
287         # we can use strict Referer checking.
288
289         # Determine the origin of this request
290         origin = request.headers.get("Origin")
291         if origin is None:
292             origin = request.referrer
293
294         # Fail if we were not able to locate an origin at all
295         if not origin:
296             return _fail("Origin checking failed - no Origin or Referer.")
297
298         # Parse our origin so we we can extract the required information from
299         # it.
300         originp = urlparse.urlparse(origin)
301
302         # Ensure that our Referer is also secure.
303         if originp.scheme != "https":
304             return _fail(
305                 "Referer checking failed - Referer is insecure while host is "
306                 "secure."
307             )
308
309         # Determine which origins we trust, which by default will include the
310         # current origin.
311         if trusted_origins is None:
312             trusted_origins = aslist(
313                 request.registry.settings.get(
314                     "pyramid.csrf_trusted_origins", [])
315             )
316
317         if request.host_port not in set(["80", "443"]):
318             trusted_origins.append("{0.domain}:{0.host_port}".format(request))
319         else:
320             trusted_origins.append(request.domain)
321
322         # Actually check to see if the request's origin matches any of our
323         # trusted origins.
324         if not any(is_same_domain(originp.netloc, host)
325                    for host in trusted_origins):
326             reason = (
327                 "Referer checking failed - {0} does not match any trusted "
328                 "origins."
329             )
330             return _fail(reason.format(origin))
331
332     return True