323 lines
12 KiB
Python
323 lines
12 KiB
Python
# -*- 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)
|