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