securecookie.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  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: 2007 Pallets
  63. :license: BSD-3-Clause
  64. """
  65. import base64
  66. import pickle
  67. import warnings
  68. from hashlib import sha1 as _default_hash
  69. from hmac import new as hmac
  70. from time import time
  71. from .._compat import iteritems
  72. from .._compat import text_type
  73. from .._compat import to_bytes
  74. from .._compat import to_native
  75. from .._internal import _date_to_unix
  76. from ..contrib.sessions import ModificationTrackingDict
  77. from ..security import safe_str_cmp
  78. from ..urls import url_quote_plus
  79. from ..urls import url_unquote_plus
  80. warnings.warn(
  81. "'werkzeug.contrib.securecookie' is deprecated as of version 0.15"
  82. " and will be removed in version 1.0. It has moved to"
  83. " https://github.com/pallets/secure-cookie.",
  84. DeprecationWarning,
  85. stacklevel=2,
  86. )
  87. class UnquoteError(Exception):
  88. """Internal exception used to signal failures on quoting."""
  89. class SecureCookie(ModificationTrackingDict):
  90. """Represents a secure cookie. You can subclass this class and provide
  91. an alternative mac method. The import thing is that the mac method
  92. is a function with a similar interface to the hashlib. Required
  93. methods are update() and digest().
  94. Example usage:
  95. >>> x = SecureCookie({"foo": 42, "baz": (1, 2, 3)}, "deadbeef")
  96. >>> x["foo"]
  97. 42
  98. >>> x["baz"]
  99. (1, 2, 3)
  100. >>> x["blafasel"] = 23
  101. >>> x.should_save
  102. True
  103. :param data: the initial data. Either a dict, list of tuples or `None`.
  104. :param secret_key: the secret key. If not set `None` or not specified
  105. it has to be set before :meth:`serialize` is called.
  106. :param new: The initial value of the `new` flag.
  107. """
  108. #: The hash method to use. This has to be a module with a new function
  109. #: or a function that creates a hashlib object. Such as `hashlib.md5`
  110. #: Subclasses can override this attribute. The default hash is sha1.
  111. #: Make sure to wrap this in staticmethod() if you store an arbitrary
  112. #: function there such as hashlib.sha1 which might be implemented
  113. #: as a function.
  114. hash_method = staticmethod(_default_hash)
  115. #: The module used for serialization. Should have a ``dumps`` and a
  116. #: ``loads`` method that takes bytes. The default is :mod:`pickle`.
  117. #:
  118. #: .. versionchanged:: 0.15
  119. #: The default of ``pickle`` will change to :mod:`json` in 1.0.
  120. serialization_method = pickle
  121. #: if the contents should be base64 quoted. This can be disabled if the
  122. #: serialization process returns cookie safe strings only.
  123. quote_base64 = True
  124. def __init__(self, data=None, secret_key=None, new=True):
  125. ModificationTrackingDict.__init__(self, data or ())
  126. # explicitly convert it into a bytestring because python 2.6
  127. # no longer performs an implicit string conversion on hmac
  128. if secret_key is not None:
  129. secret_key = to_bytes(secret_key, "utf-8")
  130. self.secret_key = secret_key
  131. self.new = new
  132. if self.serialization_method is pickle:
  133. warnings.warn(
  134. "The default 'SecureCookie.serialization_method' will"
  135. " change from pickle to json in version 1.0. To upgrade"
  136. " existing tokens, override 'unquote' to try pickle if"
  137. " json fails.",
  138. stacklevel=2,
  139. )
  140. def __repr__(self):
  141. return "<%s %s%s>" % (
  142. self.__class__.__name__,
  143. dict.__repr__(self),
  144. "*" if self.should_save else "",
  145. )
  146. @property
  147. def should_save(self):
  148. """True if the session should be saved. By default this is only true
  149. for :attr:`modified` cookies, not :attr:`new`.
  150. """
  151. return self.modified
  152. @classmethod
  153. def quote(cls, value):
  154. """Quote the value for the cookie. This can be any object supported
  155. by :attr:`serialization_method`.
  156. :param value: the value to quote.
  157. """
  158. if cls.serialization_method is not None:
  159. value = cls.serialization_method.dumps(value)
  160. if cls.quote_base64:
  161. value = b"".join(
  162. base64.b64encode(to_bytes(value, "utf8")).splitlines()
  163. ).strip()
  164. return value
  165. @classmethod
  166. def unquote(cls, value):
  167. """Unquote the value for the cookie. If unquoting does not work a
  168. :exc:`UnquoteError` is raised.
  169. :param value: the value to unquote.
  170. """
  171. try:
  172. if cls.quote_base64:
  173. value = base64.b64decode(value)
  174. if cls.serialization_method is not None:
  175. value = cls.serialization_method.loads(value)
  176. return value
  177. except Exception:
  178. # unfortunately pickle and other serialization modules can
  179. # cause pretty every error here. if we get one we catch it
  180. # and convert it into an UnquoteError
  181. raise UnquoteError()
  182. def serialize(self, expires=None):
  183. """Serialize the secure cookie into a string.
  184. If expires is provided, the session will be automatically invalidated
  185. after expiration when you unseralize it. This provides better
  186. protection against session cookie theft.
  187. :param expires: an optional expiration date for the cookie (a
  188. :class:`datetime.datetime` object)
  189. """
  190. if self.secret_key is None:
  191. raise RuntimeError("no secret key defined")
  192. if expires:
  193. self["_expires"] = _date_to_unix(expires)
  194. result = []
  195. mac = hmac(self.secret_key, None, self.hash_method)
  196. for key, value in sorted(self.items()):
  197. result.append(
  198. (
  199. "%s=%s" % (url_quote_plus(key), self.quote(value).decode("ascii"))
  200. ).encode("ascii")
  201. )
  202. mac.update(b"|" + result[-1])
  203. return b"?".join([base64.b64encode(mac.digest()).strip(), b"&".join(result)])
  204. @classmethod
  205. def unserialize(cls, string, secret_key):
  206. """Load the secure cookie from a serialized string.
  207. :param string: the cookie value to unserialize.
  208. :param secret_key: the secret key used to serialize the cookie.
  209. :return: a new :class:`SecureCookie`.
  210. """
  211. if isinstance(string, text_type):
  212. string = string.encode("utf-8", "replace")
  213. if isinstance(secret_key, text_type):
  214. secret_key = secret_key.encode("utf-8", "replace")
  215. try:
  216. base64_hash, data = string.split(b"?", 1)
  217. except (ValueError, IndexError):
  218. items = ()
  219. else:
  220. items = {}
  221. mac = hmac(secret_key, None, cls.hash_method)
  222. for item in data.split(b"&"):
  223. mac.update(b"|" + item)
  224. if b"=" not in item:
  225. items = None
  226. break
  227. key, value = item.split(b"=", 1)
  228. # try to make the key a string
  229. key = url_unquote_plus(key.decode("ascii"))
  230. try:
  231. key = to_native(key)
  232. except UnicodeError:
  233. pass
  234. items[key] = value
  235. # no parsing error and the mac looks okay, we can now
  236. # sercurely unpickle our cookie.
  237. try:
  238. client_hash = base64.b64decode(base64_hash)
  239. except TypeError:
  240. items = client_hash = None
  241. if items is not None and safe_str_cmp(client_hash, mac.digest()):
  242. try:
  243. for key, value in iteritems(items):
  244. items[key] = cls.unquote(value)
  245. except UnquoteError:
  246. items = ()
  247. else:
  248. if "_expires" in items:
  249. if time() > items["_expires"]:
  250. items = ()
  251. else:
  252. del items["_expires"]
  253. else:
  254. items = ()
  255. return cls(items, secret_key, False)
  256. @classmethod
  257. def load_cookie(cls, request, key="session", secret_key=None):
  258. """Loads a :class:`SecureCookie` from a cookie in request. If the
  259. cookie is not set, a new :class:`SecureCookie` instanced is
  260. returned.
  261. :param request: a request object that has a `cookies` attribute
  262. which is a dict of all cookie values.
  263. :param key: the name of the cookie.
  264. :param secret_key: the secret key used to unquote the cookie.
  265. Always provide the value even though it has
  266. no default!
  267. """
  268. data = request.cookies.get(key)
  269. if not data:
  270. return cls(secret_key=secret_key)
  271. return cls.unserialize(data, secret_key)
  272. def save_cookie(
  273. self,
  274. response,
  275. key="session",
  276. expires=None,
  277. session_expires=None,
  278. max_age=None,
  279. path="/",
  280. domain=None,
  281. secure=None,
  282. httponly=False,
  283. force=False,
  284. ):
  285. """Saves the SecureCookie in a cookie on response object. All
  286. parameters that are not described here are forwarded directly
  287. to :meth:`~BaseResponse.set_cookie`.
  288. :param response: a response object that has a
  289. :meth:`~BaseResponse.set_cookie` method.
  290. :param key: the name of the cookie.
  291. :param session_expires: the expiration date of the secure cookie
  292. stored information. If this is not provided
  293. the cookie `expires` date is used instead.
  294. """
  295. if force or self.should_save:
  296. data = self.serialize(session_expires or expires)
  297. response.set_cookie(
  298. key,
  299. data,
  300. expires=expires,
  301. max_age=max_age,
  302. path=path,
  303. domain=domain,
  304. secure=secure,
  305. httponly=httponly,
  306. )