aboutsummaryrefslogtreecommitdiffstats
path: root/python/werkzeug/contrib/securecookie.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/werkzeug/contrib/securecookie.py')
-rw-r--r--python/werkzeug/contrib/securecookie.py362
1 files changed, 362 insertions, 0 deletions
diff --git a/python/werkzeug/contrib/securecookie.py b/python/werkzeug/contrib/securecookie.py
new file mode 100644
index 0000000..c4c9eee
--- /dev/null
+++ b/python/werkzeug/contrib/securecookie.py
@@ -0,0 +1,362 @@
+# -*- 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: 2007 Pallets
+ :license: BSD-3-Clause
+"""
+import base64
+import pickle
+import warnings
+from hashlib import sha1 as _default_hash
+from hmac import new as hmac
+from time import time
+
+from .._compat import iteritems
+from .._compat import text_type
+from .._compat import to_bytes
+from .._compat import to_native
+from .._internal import _date_to_unix
+from ..contrib.sessions import ModificationTrackingDict
+from ..security import safe_str_cmp
+from ..urls import url_quote_plus
+from ..urls import url_unquote_plus
+
+warnings.warn(
+ "'werkzeug.contrib.securecookie' is deprecated as of version 0.15"
+ " and will be removed in version 1.0. It has moved to"
+ " https://github.com/pallets/secure-cookie.",
+ DeprecationWarning,
+ stacklevel=2,
+)
+
+
+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. Should have a ``dumps`` and a
+ #: ``loads`` method that takes bytes. The default is :mod:`pickle`.
+ #:
+ #: .. versionchanged:: 0.15
+ #: The default of ``pickle`` will change to :mod:`json` in 1.0.
+ 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
+
+ if self.serialization_method is pickle:
+ warnings.warn(
+ "The default 'SecureCookie.serialization_method' will"
+ " change from pickle to json in version 1.0. To upgrade"
+ " existing tokens, override 'unquote' to try pickle if"
+ " json fails.",
+ stacklevel=2,
+ )
+
+ def __repr__(self):
+ return "<%s %s%s>" % (
+ self.__class__.__name__,
+ dict.__repr__(self),
+ "*" if self.should_save else "",
+ )
+
+ @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(to_bytes(value, "utf8")).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,
+ )