123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323 |
- # -*- coding: utf-8 -*-
- r"""
- werkzeug.contrib.securecookie
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- This module implements a cookie that is not alterable from the client
- because it adds a checksum the server checks for. You can use it as
- session replacement if all you have is a user id or something to mark
- a logged in user.
- Keep in mind that the data is still readable from the client as a
- normal cookie is. However you don't have to store and flush the
- sessions you have at the server.
- Example usage:
- >>> from werkzeug.contrib.securecookie import SecureCookie
- >>> x = SecureCookie({"foo": 42, "baz": (1, 2, 3)}, "deadbeef")
- Dumping into a string so that one can store it in a cookie:
- >>> value = x.serialize()
- Loading from that string again:
- >>> x = SecureCookie.unserialize(value, "deadbeef")
- >>> x["baz"]
- (1, 2, 3)
- If someone modifies the cookie and the checksum is wrong the unserialize
- method will fail silently and return a new empty `SecureCookie` object.
- Keep in mind that the values will be visible in the cookie so do not
- store data in a cookie you don't want the user to see.
- Application Integration
- =======================
- If you are using the werkzeug request objects you could integrate the
- secure cookie into your application like this::
- from werkzeug.utils import cached_property
- from werkzeug.wrappers import BaseRequest
- from werkzeug.contrib.securecookie import SecureCookie
- # don't use this key but a different one; you could just use
- # os.urandom(20) to get something random
- SECRET_KEY = '\xfa\xdd\xb8z\xae\xe0}4\x8b\xea'
- class Request(BaseRequest):
- @cached_property
- def client_session(self):
- data = self.cookies.get('session_data')
- if not data:
- return SecureCookie(secret_key=SECRET_KEY)
- return SecureCookie.unserialize(data, SECRET_KEY)
- def application(environ, start_response):
- request = Request(environ)
- # get a response object here
- response = ...
- if request.client_session.should_save:
- session_data = request.client_session.serialize()
- response.set_cookie('session_data', session_data,
- httponly=True)
- return response(environ, start_response)
- A less verbose integration can be achieved by using shorthand methods::
- class Request(BaseRequest):
- @cached_property
- def client_session(self):
- return SecureCookie.load_cookie(self, secret_key=COOKIE_SECRET)
- def application(environ, start_response):
- request = Request(environ)
- # get a response object here
- response = ...
- request.client_session.save_cookie(response)
- return response(environ, start_response)
- :copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
- :license: BSD, see LICENSE for more details.
- """
- import pickle
- import base64
- from hmac import new as hmac
- from time import time
- from hashlib import sha1 as _default_hash
- from werkzeug._compat import iteritems, text_type, to_bytes
- from werkzeug.urls import url_quote_plus, url_unquote_plus
- from werkzeug._internal import _date_to_unix
- from werkzeug.contrib.sessions import ModificationTrackingDict
- from werkzeug.security import safe_str_cmp
- from werkzeug._compat import to_native
- class UnquoteError(Exception):
- """Internal exception used to signal failures on quoting."""
- class SecureCookie(ModificationTrackingDict):
- """Represents a secure cookie. You can subclass this class and provide
- an alternative mac method. The import thing is that the mac method
- is a function with a similar interface to the hashlib. Required
- methods are update() and digest().
- Example usage:
- >>> x = SecureCookie({"foo": 42, "baz": (1, 2, 3)}, "deadbeef")
- >>> x["foo"]
- 42
- >>> x["baz"]
- (1, 2, 3)
- >>> x["blafasel"] = 23
- >>> x.should_save
- True
- :param data: the initial data. Either a dict, list of tuples or `None`.
- :param secret_key: the secret key. If not set `None` or not specified
- it has to be set before :meth:`serialize` is called.
- :param new: The initial value of the `new` flag.
- """
- #: The hash method to use. This has to be a module with a new function
- #: or a function that creates a hashlib object. Such as `hashlib.md5`
- #: Subclasses can override this attribute. The default hash is sha1.
- #: Make sure to wrap this in staticmethod() if you store an arbitrary
- #: function there such as hashlib.sha1 which might be implemented
- #: as a function.
- hash_method = staticmethod(_default_hash)
- #: the module used for serialization. Unless overriden by subclasses
- #: the standard pickle module is used.
- serialization_method = pickle
- #: if the contents should be base64 quoted. This can be disabled if the
- #: serialization process returns cookie safe strings only.
- quote_base64 = True
- def __init__(self, data=None, secret_key=None, new=True):
- ModificationTrackingDict.__init__(self, data or ())
- # explicitly convert it into a bytestring because python 2.6
- # no longer performs an implicit string conversion on hmac
- if secret_key is not None:
- secret_key = to_bytes(secret_key, 'utf-8')
- self.secret_key = secret_key
- self.new = new
- def __repr__(self):
- return '<%s %s%s>' % (
- self.__class__.__name__,
- dict.__repr__(self),
- self.should_save and '*' or ''
- )
- @property
- def should_save(self):
- """True if the session should be saved. By default this is only true
- for :attr:`modified` cookies, not :attr:`new`.
- """
- return self.modified
- @classmethod
- def quote(cls, value):
- """Quote the value for the cookie. This can be any object supported
- by :attr:`serialization_method`.
- :param value: the value to quote.
- """
- if cls.serialization_method is not None:
- value = cls.serialization_method.dumps(value)
- if cls.quote_base64:
- value = b''.join(base64.b64encode(value).splitlines()).strip()
- return value
- @classmethod
- def unquote(cls, value):
- """Unquote the value for the cookie. If unquoting does not work a
- :exc:`UnquoteError` is raised.
- :param value: the value to unquote.
- """
- try:
- if cls.quote_base64:
- value = base64.b64decode(value)
- if cls.serialization_method is not None:
- value = cls.serialization_method.loads(value)
- return value
- except Exception:
- # unfortunately pickle and other serialization modules can
- # cause pretty every error here. if we get one we catch it
- # and convert it into an UnquoteError
- raise UnquoteError()
- def serialize(self, expires=None):
- """Serialize the secure cookie into a string.
- If expires is provided, the session will be automatically invalidated
- after expiration when you unseralize it. This provides better
- protection against session cookie theft.
- :param expires: an optional expiration date for the cookie (a
- :class:`datetime.datetime` object)
- """
- if self.secret_key is None:
- raise RuntimeError('no secret key defined')
- if expires:
- self['_expires'] = _date_to_unix(expires)
- result = []
- mac = hmac(self.secret_key, None, self.hash_method)
- for key, value in sorted(self.items()):
- result.append(('%s=%s' % (
- url_quote_plus(key),
- self.quote(value).decode('ascii')
- )).encode('ascii'))
- mac.update(b'|' + result[-1])
- return b'?'.join([
- base64.b64encode(mac.digest()).strip(),
- b'&'.join(result)
- ])
- @classmethod
- def unserialize(cls, string, secret_key):
- """Load the secure cookie from a serialized string.
- :param string: the cookie value to unserialize.
- :param secret_key: the secret key used to serialize the cookie.
- :return: a new :class:`SecureCookie`.
- """
- if isinstance(string, text_type):
- string = string.encode('utf-8', 'replace')
- if isinstance(secret_key, text_type):
- secret_key = secret_key.encode('utf-8', 'replace')
- try:
- base64_hash, data = string.split(b'?', 1)
- except (ValueError, IndexError):
- items = ()
- else:
- items = {}
- mac = hmac(secret_key, None, cls.hash_method)
- for item in data.split(b'&'):
- mac.update(b'|' + item)
- if b'=' not in item:
- items = None
- break
- key, value = item.split(b'=', 1)
- # try to make the key a string
- key = url_unquote_plus(key.decode('ascii'))
- try:
- key = to_native(key)
- except UnicodeError:
- pass
- items[key] = value
- # no parsing error and the mac looks okay, we can now
- # sercurely unpickle our cookie.
- try:
- client_hash = base64.b64decode(base64_hash)
- except TypeError:
- items = client_hash = None
- if items is not None and safe_str_cmp(client_hash, mac.digest()):
- try:
- for key, value in iteritems(items):
- items[key] = cls.unquote(value)
- except UnquoteError:
- items = ()
- else:
- if '_expires' in items:
- if time() > items['_expires']:
- items = ()
- else:
- del items['_expires']
- else:
- items = ()
- return cls(items, secret_key, False)
- @classmethod
- def load_cookie(cls, request, key='session', secret_key=None):
- """Loads a :class:`SecureCookie` from a cookie in request. If the
- cookie is not set, a new :class:`SecureCookie` instanced is
- returned.
- :param request: a request object that has a `cookies` attribute
- which is a dict of all cookie values.
- :param key: the name of the cookie.
- :param secret_key: the secret key used to unquote the cookie.
- Always provide the value even though it has
- no default!
- """
- data = request.cookies.get(key)
- if not data:
- return cls(secret_key=secret_key)
- return cls.unserialize(data, secret_key)
- def save_cookie(self, response, key='session', expires=None,
- session_expires=None, max_age=None, path='/', domain=None,
- secure=None, httponly=False, force=False):
- """Saves the SecureCookie in a cookie on response object. All
- parameters that are not described here are forwarded directly
- to :meth:`~BaseResponse.set_cookie`.
- :param response: a response object that has a
- :meth:`~BaseResponse.set_cookie` method.
- :param key: the name of the cookie.
- :param session_expires: the expiration date of the secure cookie
- stored information. If this is not provided
- the cookie `expires` date is used instead.
- """
- if force or self.should_save:
- data = self.serialize(session_expires or expires)
- response.set_cookie(key, data, expires=expires, max_age=max_age,
- path=path, domain=domain, secure=secure,
- httponly=httponly)
|