securecookie.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. # -*- coding: utf-8 -*-
  2. r"""
  3. werkzeug.contrib.securecookie
  4. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  5. This module implements a cookie that is not alterable from the client
  6. because it adds a checksum the server checks for. You can use it as
  7. session replacement if all you have is a user id or something to mark
  8. a logged in user.
  9. Keep in mind that the data is still readable from the client as a
  10. normal cookie is. However you don't have to store and flush the
  11. sessions you have at the server.
  12. Example usage:
  13. >>> from werkzeug.contrib.securecookie import SecureCookie
  14. >>> x = SecureCookie({"foo": 42, "baz": (1, 2, 3)}, "deadbeef")
  15. Dumping into a string so that one can store it in a cookie:
  16. >>> value = x.serialize()
  17. Loading from that string again:
  18. >>> x = SecureCookie.unserialize(value, "deadbeef")
  19. >>> x["baz"]
  20. (1, 2, 3)
  21. If someone modifies the cookie and the checksum is wrong the unserialize
  22. method will fail silently and return a new empty `SecureCookie` object.
  23. Keep in mind that the values will be visible in the cookie so do not
  24. store data in a cookie you don't want the user to see.
  25. Application Integration
  26. =======================
  27. If you are using the werkzeug request objects you could integrate the
  28. secure cookie into your application like this::
  29. from werkzeug.utils import cached_property
  30. from werkzeug.wrappers import BaseRequest
  31. from werkzeug.contrib.securecookie import SecureCookie
  32. # don't use this key but a different one; you could just use
  33. # os.urandom(20) to get something random
  34. SECRET_KEY = '\xfa\xdd\xb8z\xae\xe0}4\x8b\xea'
  35. class Request(BaseRequest):
  36. @cached_property
  37. def client_session(self):
  38. data = self.cookies.get('session_data')
  39. if not data:
  40. return SecureCookie(secret_key=SECRET_KEY)
  41. return SecureCookie.unserialize(data, SECRET_KEY)
  42. def application(environ, start_response):
  43. request = Request(environ)
  44. # get a response object here
  45. response = ...
  46. if request.client_session.should_save:
  47. session_data = request.client_session.serialize()
  48. response.set_cookie('session_data', session_data,
  49. httponly=True)
  50. return response(environ, start_response)
  51. A less verbose integration can be achieved by using shorthand methods::
  52. class Request(BaseRequest):
  53. @cached_property
  54. def client_session(self):
  55. return SecureCookie.load_cookie(self, secret_key=COOKIE_SECRET)
  56. def application(environ, start_response):
  57. request = Request(environ)
  58. # get a response object here
  59. response = ...
  60. request.client_session.save_cookie(response)
  61. return response(environ, start_response)
  62. :copyright: (c) 2014 by the Werkzeug Team, see AUTHORS for more details.
  63. :license: BSD, see LICENSE for more details.
  64. """
  65. import pickle
  66. import base64
  67. from hmac import new as hmac
  68. from time import time
  69. from hashlib import sha1 as _default_hash
  70. from werkzeug._compat import iteritems, text_type, to_bytes
  71. from werkzeug.urls import url_quote_plus, url_unquote_plus
  72. from werkzeug._internal import _date_to_unix
  73. from werkzeug.contrib.sessions import ModificationTrackingDict
  74. from werkzeug.security import safe_str_cmp
  75. from werkzeug._compat import to_native
  76. class UnquoteError(Exception):
  77. """Internal exception used to signal failures on quoting."""
  78. class SecureCookie(ModificationTrackingDict):
  79. """Represents a secure cookie. You can subclass this class and provide
  80. an alternative mac method. The import thing is that the mac method
  81. is a function with a similar interface to the hashlib. Required
  82. methods are update() and digest().
  83. Example usage:
  84. >>> x = SecureCookie({"foo": 42, "baz": (1, 2, 3)}, "deadbeef")
  85. >>> x["foo"]
  86. 42
  87. >>> x["baz"]
  88. (1, 2, 3)
  89. >>> x["blafasel"] = 23
  90. >>> x.should_save
  91. True
  92. :param data: the initial data. Either a dict, list of tuples or `None`.
  93. :param secret_key: the secret key. If not set `None` or not specified
  94. it has to be set before :meth:`serialize` is called.
  95. :param new: The initial value of the `new` flag.
  96. """
  97. #: The hash method to use. This has to be a module with a new function
  98. #: or a function that creates a hashlib object. Such as `hashlib.md5`
  99. #: Subclasses can override this attribute. The default hash is sha1.
  100. #: Make sure to wrap this in staticmethod() if you store an arbitrary
  101. #: function there such as hashlib.sha1 which might be implemented
  102. #: as a function.
  103. hash_method = staticmethod(_default_hash)
  104. #: the module used for serialization. Unless overriden by subclasses
  105. #: the standard pickle module is used.
  106. serialization_method = pickle
  107. #: if the contents should be base64 quoted. This can be disabled if the
  108. #: serialization process returns cookie safe strings only.
  109. quote_base64 = True
  110. def __init__(self, data=None, secret_key=None, new=True):
  111. ModificationTrackingDict.__init__(self, data or ())
  112. # explicitly convert it into a bytestring because python 2.6
  113. # no longer performs an implicit string conversion on hmac
  114. if secret_key is not None:
  115. secret_key = to_bytes(secret_key, 'utf-8')
  116. self.secret_key = secret_key
  117. self.new = new
  118. def __repr__(self):
  119. return '<%s %s%s>' % (
  120. self.__class__.__name__,
  121. dict.__repr__(self),
  122. self.should_save and '*' or ''
  123. )
  124. @property
  125. def should_save(self):
  126. """True if the session should be saved. By default this is only true
  127. for :attr:`modified` cookies, not :attr:`new`.
  128. """
  129. return self.modified
  130. @classmethod
  131. def quote(cls, value):
  132. """Quote the value for the cookie. This can be any object supported
  133. by :attr:`serialization_method`.
  134. :param value: the value to quote.
  135. """
  136. if cls.serialization_method is not None:
  137. value = cls.serialization_method.dumps(value)
  138. if cls.quote_base64:
  139. value = b''.join(base64.b64encode(value).splitlines()).strip()
  140. return value
  141. @classmethod
  142. def unquote(cls, value):
  143. """Unquote the value for the cookie. If unquoting does not work a
  144. :exc:`UnquoteError` is raised.
  145. :param value: the value to unquote.
  146. """
  147. try:
  148. if cls.quote_base64:
  149. value = base64.b64decode(value)
  150. if cls.serialization_method is not None:
  151. value = cls.serialization_method.loads(value)
  152. return value
  153. except Exception:
  154. # unfortunately pickle and other serialization modules can
  155. # cause pretty every error here. if we get one we catch it
  156. # and convert it into an UnquoteError
  157. raise UnquoteError()
  158. def serialize(self, expires=None):
  159. """Serialize the secure cookie into a string.
  160. If expires is provided, the session will be automatically invalidated
  161. after expiration when you unseralize it. This provides better
  162. protection against session cookie theft.
  163. :param expires: an optional expiration date for the cookie (a
  164. :class:`datetime.datetime` object)
  165. """
  166. if self.secret_key is None:
  167. raise RuntimeError('no secret key defined')
  168. if expires:
  169. self['_expires'] = _date_to_unix(expires)
  170. result = []
  171. mac = hmac(self.secret_key, None, self.hash_method)
  172. for key, value in sorted(self.items()):
  173. result.append(('%s=%s' % (
  174. url_quote_plus(key),
  175. self.quote(value).decode('ascii')
  176. )).encode('ascii'))
  177. mac.update(b'|' + result[-1])
  178. return b'?'.join([
  179. base64.b64encode(mac.digest()).strip(),
  180. b'&'.join(result)
  181. ])
  182. @classmethod
  183. def unserialize(cls, string, secret_key):
  184. """Load the secure cookie from a serialized string.
  185. :param string: the cookie value to unserialize.
  186. :param secret_key: the secret key used to serialize the cookie.
  187. :return: a new :class:`SecureCookie`.
  188. """
  189. if isinstance(string, text_type):
  190. string = string.encode('utf-8', 'replace')
  191. if isinstance(secret_key, text_type):
  192. secret_key = secret_key.encode('utf-8', 'replace')
  193. try:
  194. base64_hash, data = string.split(b'?', 1)
  195. except (ValueError, IndexError):
  196. items = ()
  197. else:
  198. items = {}
  199. mac = hmac(secret_key, None, cls.hash_method)
  200. for item in data.split(b'&'):
  201. mac.update(b'|' + item)
  202. if b'=' not in item:
  203. items = None
  204. break
  205. key, value = item.split(b'=', 1)
  206. # try to make the key a string
  207. key = url_unquote_plus(key.decode('ascii'))
  208. try:
  209. key = to_native(key)
  210. except UnicodeError:
  211. pass
  212. items[key] = value
  213. # no parsing error and the mac looks okay, we can now
  214. # sercurely unpickle our cookie.
  215. try:
  216. client_hash = base64.b64decode(base64_hash)
  217. except TypeError:
  218. items = client_hash = None
  219. if items is not None and safe_str_cmp(client_hash, mac.digest()):
  220. try:
  221. for key, value in iteritems(items):
  222. items[key] = cls.unquote(value)
  223. except UnquoteError:
  224. items = ()
  225. else:
  226. if '_expires' in items:
  227. if time() > items['_expires']:
  228. items = ()
  229. else:
  230. del items['_expires']
  231. else:
  232. items = ()
  233. return cls(items, secret_key, False)
  234. @classmethod
  235. def load_cookie(cls, request, key='session', secret_key=None):
  236. """Loads a :class:`SecureCookie` from a cookie in request. If the
  237. cookie is not set, a new :class:`SecureCookie` instanced is
  238. returned.
  239. :param request: a request object that has a `cookies` attribute
  240. which is a dict of all cookie values.
  241. :param key: the name of the cookie.
  242. :param secret_key: the secret key used to unquote the cookie.
  243. Always provide the value even though it has
  244. no default!
  245. """
  246. data = request.cookies.get(key)
  247. if not data:
  248. return cls(secret_key=secret_key)
  249. return cls.unserialize(data, secret_key)
  250. def save_cookie(self, response, key='session', expires=None,
  251. session_expires=None, max_age=None, path='/', domain=None,
  252. secure=None, httponly=False, force=False):
  253. """Saves the SecureCookie in a cookie on response object. All
  254. parameters that are not described here are forwarded directly
  255. to :meth:`~BaseResponse.set_cookie`.
  256. :param response: a response object that has a
  257. :meth:`~BaseResponse.set_cookie` method.
  258. :param key: the name of the cookie.
  259. :param session_expires: the expiration date of the secure cookie
  260. stored information. If this is not provided
  261. the cookie `expires` date is used instead.
  262. """
  263. if force or self.should_save:
  264. data = self.serialize(session_expires or expires)
  265. response.set_cookie(key, data, expires=expires, max_age=max_age,
  266. path=path, domain=domain, secure=secure,
  267. httponly=httponly)