Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/pyramid/session.py : 17%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import base64
2import binascii
3import hashlib
4import hmac
5import os
6import time
7import warnings
9from zope.deprecation import deprecated
10from zope.interface import implementer
12from webob.cookies import JSONSerializer, SignedSerializer
14from pyramid.compat import pickle, PY2, text_, bytes_, native_
15from pyramid.csrf import check_csrf_origin, check_csrf_token
17from pyramid.interfaces import ISession
18from pyramid.util import strings_differ
21def manage_accessed(wrapped):
22 """ Decorator which causes a cookie to be renewed when an accessor
23 method is called."""
25 def accessed(session, *arg, **kw):
26 session.accessed = now = int(time.time())
27 if session._reissue_time is not None:
28 if now - session.renewed > session._reissue_time:
29 session.changed()
30 return wrapped(session, *arg, **kw)
32 accessed.__doc__ = wrapped.__doc__
33 return accessed
36def manage_changed(wrapped):
37 """ Decorator which causes a cookie to be set when a setter method
38 is called."""
40 def changed(session, *arg, **kw):
41 session.accessed = int(time.time())
42 session.changed()
43 return wrapped(session, *arg, **kw)
45 changed.__doc__ = wrapped.__doc__
46 return changed
49def signed_serialize(data, secret):
50 """ Serialize any pickleable structure (``data``) and sign it
51 using the ``secret`` (must be a string). Return the
52 serialization, which includes the signature as its first 40 bytes.
53 The ``signed_deserialize`` method will deserialize such a value.
55 This function is useful for creating signed cookies. For example:
57 .. code-block:: python
59 cookieval = signed_serialize({'a':1}, 'secret')
60 response.set_cookie('signed_cookie', cookieval)
62 .. deprecated:: 1.10
64 This function will be removed in :app:`Pyramid` 2.0. It is using
65 pickle-based serialization, which is considered vulnerable to remote
66 code execution attacks and will no longer be used by the default
67 session factories at that time.
69 """
70 pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL)
71 try:
72 # bw-compat with pyramid <= 1.5b1 where latin1 is the default
73 secret = bytes_(secret)
74 except UnicodeEncodeError:
75 secret = bytes_(secret, 'utf-8')
76 sig = hmac.new(secret, pickled, hashlib.sha1).hexdigest()
77 return sig + native_(base64.b64encode(pickled))
80deprecated(
81 'signed_serialize',
82 'This function will be removed in Pyramid 2.0. It is using pickle-based '
83 'serialization, which is considered vulnerable to remote code execution '
84 'attacks.',
85)
88def signed_deserialize(serialized, secret, hmac=hmac):
89 """ Deserialize the value returned from ``signed_serialize``. If
90 the value cannot be deserialized for any reason, a
91 :exc:`ValueError` exception will be raised.
93 This function is useful for deserializing a signed cookie value
94 created by ``signed_serialize``. For example:
96 .. code-block:: python
98 cookieval = request.cookies['signed_cookie']
99 data = signed_deserialize(cookieval, 'secret')
101 .. deprecated:: 1.10
103 This function will be removed in :app:`Pyramid` 2.0. It is using
104 pickle-based serialization, which is considered vulnerable to remote
105 code execution attacks and will no longer be used by the default
106 session factories at that time.
107 """
108 # hmac parameterized only for unit tests
109 try:
110 input_sig, pickled = (
111 bytes_(serialized[:40]),
112 base64.b64decode(bytes_(serialized[40:])),
113 )
114 except (binascii.Error, TypeError) as e:
115 # Badly formed data can make base64 die
116 raise ValueError('Badly formed base64 data: %s' % e)
118 try:
119 # bw-compat with pyramid <= 1.5b1 where latin1 is the default
120 secret = bytes_(secret)
121 except UnicodeEncodeError:
122 secret = bytes_(secret, 'utf-8')
123 sig = bytes_(hmac.new(secret, pickled, hashlib.sha1).hexdigest())
125 # Avoid timing attacks (see
126 # http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf)
127 if strings_differ(sig, input_sig):
128 raise ValueError('Invalid signature')
130 return pickle.loads(pickled)
133deprecated(
134 'signed_deserialize',
135 'This function will be removed in Pyramid 2.0. It is using pickle-based '
136 'serialization, which is considered vulnerable to remote code execution '
137 'attacks.',
138)
141class PickleSerializer(object):
142 """ A serializer that uses the pickle protocol to dump Python
143 data to bytes.
145 This is the default serializer used by Pyramid.
147 ``protocol`` may be specified to control the version of pickle used.
148 Defaults to :attr:`pickle.HIGHEST_PROTOCOL`.
150 """
152 def __init__(self, protocol=pickle.HIGHEST_PROTOCOL):
153 self.protocol = protocol
155 def loads(self, bstruct):
156 """Accept bytes and return a Python object."""
157 try:
158 return pickle.loads(bstruct)
159 # at least ValueError, AttributeError, ImportError but more to be safe
160 except Exception:
161 raise ValueError
163 def dumps(self, appstruct):
164 """Accept a Python object and return bytes."""
165 return pickle.dumps(appstruct, self.protocol)
168JSONSerializer = JSONSerializer # api
171def BaseCookieSessionFactory(
172 serializer,
173 cookie_name='session',
174 max_age=None,
175 path='/',
176 domain=None,
177 secure=False,
178 httponly=False,
179 samesite='Lax',
180 timeout=1200,
181 reissue_time=0,
182 set_on_exception=True,
183):
184 """
185 Configure a :term:`session factory` which will provide cookie-based
186 sessions. The return value of this function is a :term:`session factory`,
187 which may be provided as the ``session_factory`` argument of a
188 :class:`pyramid.config.Configurator` constructor, or used as the
189 ``session_factory`` argument of the
190 :meth:`pyramid.config.Configurator.set_session_factory` method.
192 The session factory returned by this function will create sessions
193 which are limited to storing fewer than 4000 bytes of data (as the
194 payload must fit into a single cookie).
196 .. warning:
198 This class provides no protection from tampering and is only intended
199 to be used by framework authors to create their own cookie-based
200 session factories.
202 Parameters:
204 ``serializer``
205 An object with two methods: ``loads`` and ``dumps``. The ``loads``
206 method should accept bytes and return a Python object. The ``dumps``
207 method should accept a Python object and return bytes. A ``ValueError``
208 should be raised for malformed inputs.
210 ``cookie_name``
211 The name of the cookie used for sessioning. Default: ``'session'``.
213 ``max_age``
214 The maximum age of the cookie used for sessioning (in seconds).
215 Default: ``None`` (browser scope).
217 ``path``
218 The path used for the session cookie. Default: ``'/'``.
220 ``domain``
221 The domain used for the session cookie. Default: ``None`` (no domain).
223 ``secure``
224 The 'secure' flag of the session cookie. Default: ``False``.
226 ``httponly``
227 Hide the cookie from Javascript by setting the 'HttpOnly' flag of the
228 session cookie. Default: ``False``.
230 ``samesite``
231 The 'samesite' option of the session cookie. Set the value to ``None``
232 to turn off the samesite option. Default: ``'Lax'``.
234 ``timeout``
235 A number of seconds of inactivity before a session times out. If
236 ``None`` then the cookie never expires. This lifetime only applies
237 to the *value* within the cookie. Meaning that if the cookie expires
238 due to a lower ``max_age``, then this setting has no effect.
239 Default: ``1200``.
241 ``reissue_time``
242 The number of seconds that must pass before the cookie is automatically
243 reissued as the result of a request which accesses the session. The
244 duration is measured as the number of seconds since the last session
245 cookie was issued and 'now'. If this value is ``0``, a new cookie
246 will be reissued on every request accessing the session. If ``None``
247 then the cookie's lifetime will never be extended.
249 A good rule of thumb: if you want auto-expired cookies based on
250 inactivity: set the ``timeout`` value to 1200 (20 mins) and set the
251 ``reissue_time`` value to perhaps a tenth of the ``timeout`` value
252 (120 or 2 mins). It's nonsensical to set the ``timeout`` value lower
253 than the ``reissue_time`` value, as the ticket will never be reissued.
254 However, such a configuration is not explicitly prevented.
256 Default: ``0``.
258 ``set_on_exception``
259 If ``True``, set a session cookie even if an exception occurs
260 while rendering a view. Default: ``True``.
262 .. versionadded: 1.5a3
264 .. versionchanged: 1.10
266 Added the ``samesite`` option and made the default ``'Lax'``.
267 """
269 @implementer(ISession)
270 class CookieSession(dict):
271 """ Dictionary-like session object """
273 # configuration parameters
274 _cookie_name = cookie_name
275 _cookie_max_age = max_age if max_age is None else int(max_age)
276 _cookie_path = path
277 _cookie_domain = domain
278 _cookie_secure = secure
279 _cookie_httponly = httponly
280 _cookie_samesite = samesite
281 _cookie_on_exception = set_on_exception
282 _timeout = timeout if timeout is None else int(timeout)
283 _reissue_time = (
284 reissue_time if reissue_time is None else int(reissue_time)
285 )
287 # dirty flag
288 _dirty = False
290 def __init__(self, request):
291 self.request = request
292 now = time.time()
293 created = renewed = now
294 new = True
295 value = None
296 state = {}
297 cookieval = request.cookies.get(self._cookie_name)
298 if cookieval is not None:
299 try:
300 value = serializer.loads(bytes_(cookieval))
301 except ValueError:
302 # the cookie failed to deserialize, dropped
303 value = None
305 if value is not None:
306 try:
307 # since the value is not necessarily signed, we have
308 # to unpack it a little carefully
309 rval, cval, sval = value
310 renewed = float(rval)
311 created = float(cval)
312 state = sval
313 new = False
314 except (TypeError, ValueError):
315 # value failed to unpack properly or renewed was not
316 # a numeric type so we'll fail deserialization here
317 state = {}
319 if self._timeout is not None:
320 if now - renewed > self._timeout:
321 # expire the session because it was not renewed
322 # before the timeout threshold
323 state = {}
325 self.created = created
326 self.accessed = renewed
327 self.renewed = renewed
328 self.new = new
329 dict.__init__(self, state)
331 # ISession methods
332 def changed(self):
333 if not self._dirty:
334 self._dirty = True
336 def set_cookie_callback(request, response):
337 self._set_cookie(response)
338 self.request = None # explicitly break cycle for gc
340 self.request.add_response_callback(set_cookie_callback)
342 def invalidate(self):
343 self.clear() # XXX probably needs to unset cookie
345 # non-modifying dictionary methods
346 get = manage_accessed(dict.get)
347 __getitem__ = manage_accessed(dict.__getitem__)
348 items = manage_accessed(dict.items)
349 values = manage_accessed(dict.values)
350 keys = manage_accessed(dict.keys)
351 __contains__ = manage_accessed(dict.__contains__)
352 __len__ = manage_accessed(dict.__len__)
353 __iter__ = manage_accessed(dict.__iter__)
355 if PY2:
356 iteritems = manage_accessed(dict.iteritems)
357 itervalues = manage_accessed(dict.itervalues)
358 iterkeys = manage_accessed(dict.iterkeys)
359 has_key = manage_accessed(dict.has_key)
361 # modifying dictionary methods
362 clear = manage_changed(dict.clear)
363 update = manage_changed(dict.update)
364 setdefault = manage_changed(dict.setdefault)
365 pop = manage_changed(dict.pop)
366 popitem = manage_changed(dict.popitem)
367 __setitem__ = manage_changed(dict.__setitem__)
368 __delitem__ = manage_changed(dict.__delitem__)
370 # flash API methods
371 @manage_changed
372 def flash(self, msg, queue='', allow_duplicate=True):
373 storage = self.setdefault('_f_' + queue, [])
374 if allow_duplicate or (msg not in storage):
375 storage.append(msg)
377 @manage_changed
378 def pop_flash(self, queue=''):
379 storage = self.pop('_f_' + queue, [])
380 return storage
382 @manage_accessed
383 def peek_flash(self, queue=''):
384 storage = self.get('_f_' + queue, [])
385 return storage
387 # CSRF API methods
388 @manage_changed
389 def new_csrf_token(self):
390 token = text_(binascii.hexlify(os.urandom(20)))
391 self['_csrft_'] = token
392 return token
394 @manage_accessed
395 def get_csrf_token(self):
396 token = self.get('_csrft_', None)
397 if token is None:
398 token = self.new_csrf_token()
399 return token
401 # non-API methods
402 def _set_cookie(self, response):
403 if not self._cookie_on_exception:
404 exception = getattr(self.request, 'exception', None)
405 if (
406 exception is not None
407 ): # dont set a cookie during exceptions
408 return False
409 cookieval = native_(
410 serializer.dumps((self.accessed, self.created, dict(self)))
411 )
412 if len(cookieval) > 4064:
413 raise ValueError(
414 'Cookie value is too long to store (%s bytes)'
415 % len(cookieval)
416 )
417 response.set_cookie(
418 self._cookie_name,
419 value=cookieval,
420 max_age=self._cookie_max_age,
421 path=self._cookie_path,
422 domain=self._cookie_domain,
423 secure=self._cookie_secure,
424 httponly=self._cookie_httponly,
425 samesite=self._cookie_samesite,
426 )
427 return True
429 return CookieSession
432def UnencryptedCookieSessionFactoryConfig(
433 secret,
434 timeout=1200,
435 cookie_name='session',
436 cookie_max_age=None,
437 cookie_path='/',
438 cookie_domain=None,
439 cookie_secure=False,
440 cookie_httponly=False,
441 cookie_samesite='Lax',
442 cookie_on_exception=True,
443 signed_serialize=signed_serialize,
444 signed_deserialize=signed_deserialize,
445):
446 """
447 .. deprecated:: 1.5
448 Use :func:`pyramid.session.SignedCookieSessionFactory` instead.
449 Caveat: Cookies generated using ``SignedCookieSessionFactory`` are not
450 compatible with cookies generated using
451 ``UnencryptedCookieSessionFactory``, so existing user session data
452 will be destroyed if you switch to it.
454 Configure a :term:`session factory` which will provide unencrypted
455 (but signed) cookie-based sessions. The return value of this
456 function is a :term:`session factory`, which may be provided as
457 the ``session_factory`` argument of a
458 :class:`pyramid.config.Configurator` constructor, or used
459 as the ``session_factory`` argument of the
460 :meth:`pyramid.config.Configurator.set_session_factory`
461 method.
463 The session factory returned by this function will create sessions
464 which are limited to storing fewer than 4000 bytes of data (as the
465 payload must fit into a single cookie).
467 Parameters:
469 ``secret``
470 A string which is used to sign the cookie.
472 ``timeout``
473 A number of seconds of inactivity before a session times out.
475 ``cookie_name``
476 The name of the cookie used for sessioning.
478 ``cookie_max_age``
479 The maximum age of the cookie used for sessioning (in seconds).
480 Default: ``None`` (browser scope).
482 ``cookie_path``
483 The path used for the session cookie.
485 ``cookie_domain``
486 The domain used for the session cookie. Default: ``None`` (no domain).
488 ``cookie_secure``
489 The 'secure' flag of the session cookie.
491 ``cookie_httponly``
492 The 'httpOnly' flag of the session cookie.
494 ``cookie_samesite``
495 The 'samesite' option of the session cookie. Set the value to ``None``
496 to turn off the samesite option. Default: ``'Lax'``.
498 ``cookie_on_exception``
499 If ``True``, set a session cookie even if an exception occurs
500 while rendering a view.
502 ``signed_serialize``
503 A callable which takes more or less arbitrary Python data structure and
504 a secret and returns a signed serialization in bytes.
505 Default: ``signed_serialize`` (using pickle).
507 ``signed_deserialize``
508 A callable which takes a signed and serialized data structure in bytes
509 and a secret and returns the original data structure if the signature
510 is valid. Default: ``signed_deserialize`` (using pickle).
512 .. versionchanged: 1.10
514 Added the ``samesite`` option and made the default ``'Lax'``.
515 """
517 class SerializerWrapper(object):
518 def __init__(self, secret):
519 self.secret = secret
521 def loads(self, bstruct):
522 return signed_deserialize(bstruct, secret)
524 def dumps(self, appstruct):
525 return signed_serialize(appstruct, secret)
527 serializer = SerializerWrapper(secret)
529 return BaseCookieSessionFactory(
530 serializer,
531 cookie_name=cookie_name,
532 max_age=cookie_max_age,
533 path=cookie_path,
534 domain=cookie_domain,
535 secure=cookie_secure,
536 httponly=cookie_httponly,
537 samesite=cookie_samesite,
538 timeout=timeout,
539 reissue_time=0, # to keep session.accessed == session.renewed
540 set_on_exception=cookie_on_exception,
541 )
544deprecated(
545 'UnencryptedCookieSessionFactoryConfig',
546 'The UnencryptedCookieSessionFactoryConfig callable is deprecated as of '
547 'Pyramid 1.5. Use ``pyramid.session.SignedCookieSessionFactory`` instead.'
548 ' Caveat: Cookies generated using SignedCookieSessionFactory are not '
549 'compatible with cookies generated using UnencryptedCookieSessionFactory, '
550 'so existing user session data will be destroyed if you switch to it.',
551)
554def SignedCookieSessionFactory(
555 secret,
556 cookie_name='session',
557 max_age=None,
558 path='/',
559 domain=None,
560 secure=False,
561 httponly=False,
562 samesite='Lax',
563 set_on_exception=True,
564 timeout=1200,
565 reissue_time=0,
566 hashalg='sha512',
567 salt='pyramid.session.',
568 serializer=None,
569):
570 """
571 .. versionadded:: 1.5
573 Configure a :term:`session factory` which will provide signed
574 cookie-based sessions. The return value of this
575 function is a :term:`session factory`, which may be provided as
576 the ``session_factory`` argument of a
577 :class:`pyramid.config.Configurator` constructor, or used
578 as the ``session_factory`` argument of the
579 :meth:`pyramid.config.Configurator.set_session_factory`
580 method.
582 The session factory returned by this function will create sessions
583 which are limited to storing fewer than 4000 bytes of data (as the
584 payload must fit into a single cookie).
586 Parameters:
588 ``secret``
589 A string which is used to sign the cookie. The secret should be at
590 least as long as the block size of the selected hash algorithm. For
591 ``sha512`` this would mean a 512 bit (64 character) secret. It should
592 be unique within the set of secret values provided to Pyramid for
593 its various subsystems (see :ref:`admonishment_against_secret_sharing`).
595 ``hashalg``
596 The HMAC digest algorithm to use for signing. The algorithm must be
597 supported by the :mod:`hashlib` library. Default: ``'sha512'``.
599 ``salt``
600 A namespace to avoid collisions between different uses of a shared
601 secret. Reusing a secret for different parts of an application is
602 strongly discouraged (see :ref:`admonishment_against_secret_sharing`).
603 Default: ``'pyramid.session.'``.
605 ``cookie_name``
606 The name of the cookie used for sessioning. Default: ``'session'``.
608 ``max_age``
609 The maximum age of the cookie used for sessioning (in seconds).
610 Default: ``None`` (browser scope).
612 ``path``
613 The path used for the session cookie. Default: ``'/'``.
615 ``domain``
616 The domain used for the session cookie. Default: ``None`` (no domain).
618 ``secure``
619 The 'secure' flag of the session cookie. Default: ``False``.
621 ``httponly``
622 Hide the cookie from Javascript by setting the 'HttpOnly' flag of the
623 session cookie. Default: ``False``.
625 ``samesite``
626 The 'samesite' option of the session cookie. Set the value to ``None``
627 to turn off the samesite option. Default: ``'Lax'``.
629 ``timeout``
630 A number of seconds of inactivity before a session times out. If
631 ``None`` then the cookie never expires. This lifetime only applies
632 to the *value* within the cookie. Meaning that if the cookie expires
633 due to a lower ``max_age``, then this setting has no effect.
634 Default: ``1200``.
636 ``reissue_time``
637 The number of seconds that must pass before the cookie is automatically
638 reissued as the result of accessing the session. The
639 duration is measured as the number of seconds since the last session
640 cookie was issued and 'now'. If this value is ``0``, a new cookie
641 will be reissued on every request accessing the session. If ``None``
642 then the cookie's lifetime will never be extended.
644 A good rule of thumb: if you want auto-expired cookies based on
645 inactivity: set the ``timeout`` value to 1200 (20 mins) and set the
646 ``reissue_time`` value to perhaps a tenth of the ``timeout`` value
647 (120 or 2 mins). It's nonsensical to set the ``timeout`` value lower
648 than the ``reissue_time`` value, as the ticket will never be reissued.
649 However, such a configuration is not explicitly prevented.
651 Default: ``0``.
653 ``set_on_exception``
654 If ``True``, set a session cookie even if an exception occurs
655 while rendering a view. Default: ``True``.
657 ``serializer``
658 An object with two methods: ``loads`` and ``dumps``. The ``loads``
659 method should accept bytes and return a Python object. The ``dumps``
660 method should accept a Python object and return bytes. A ``ValueError``
661 should be raised for malformed inputs. If a serializer is not passed,
662 the :class:`pyramid.session.PickleSerializer` serializer will be used.
664 .. warning::
666 In :app:`Pyramid` 2.0 the default ``serializer`` option will change to
667 use :class:`pyramid.session.JSONSerializer`. See
668 :ref:`pickle_session_deprecation` for more information about why this
669 change is being made.
671 .. versionadded: 1.5a3
673 .. versionchanged: 1.10
675 Added the ``samesite`` option and made the default ``Lax``.
677 """
678 if serializer is None:
679 serializer = PickleSerializer()
680 warnings.warn(
681 'The default pickle serializer is deprecated as of Pyramid 1.9 '
682 'and it will be changed to use pyramid.session.JSONSerializer in '
683 'version 2.0. Explicitly set the serializer to avoid future '
684 'incompatibilities. See "Upcoming Changes to ISession in '
685 'Pyramid 2.0" for more information about this change.',
686 DeprecationWarning,
687 stacklevel=1,
688 )
690 signed_serializer = SignedSerializer(
691 secret, salt, hashalg, serializer=serializer
692 )
694 return BaseCookieSessionFactory(
695 signed_serializer,
696 cookie_name=cookie_name,
697 max_age=max_age,
698 path=path,
699 domain=domain,
700 secure=secure,
701 httponly=httponly,
702 samesite=samesite,
703 timeout=timeout,
704 reissue_time=reissue_time,
705 set_on_exception=set_on_exception,
706 )
709check_csrf_origin = check_csrf_origin # api
710deprecated(
711 'check_csrf_origin',
712 'pyramid.session.check_csrf_origin is deprecated as of Pyramid '
713 '1.9. Use pyramid.csrf.check_csrf_origin instead.',
714)
716check_csrf_token = check_csrf_token # api
717deprecated(
718 'check_csrf_token',
719 'pyramid.session.check_csrf_token is deprecated as of Pyramid '
720 '1.9. Use pyramid.csrf.check_csrf_token instead.',
721)