From 5b5b06a67a0586b7371442402b1dac54dc508919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Blondon?= Date: Fri, 3 Apr 2020 17:54:50 +0200 Subject: [PATCH 1/8] extract mac unserialization implementation --- src/secure_cookie/cookie.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/secure_cookie/cookie.py b/src/secure_cookie/cookie.py index 7030552..f3695fc 100644 --- a/src/secure_cookie/cookie.py +++ b/src/secure_cookie/cookie.py @@ -309,6 +309,10 @@ def unserialize(cls, string, secret_key): if isinstance(secret_key, text_type): secret_key = secret_key.encode("utf-8", "replace") + return cls._mac_unserialize(string, secret_key) + + @classmethod + def _mac_unserialize(cls, string, secret_key): try: base64_hash, data = string.split(b"?", 1) except (ValueError, IndexError): From 0e1a70ab0ef640968c6c01dc9d591ea77c45f90d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Blondon?= Date: Fri, 3 Apr 2020 18:11:14 +0200 Subject: [PATCH 2/8] extract mac serialization implementation --- src/secure_cookie/cookie.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/secure_cookie/cookie.py b/src/secure_cookie/cookie.py index f3695fc..9343f98 100644 --- a/src/secure_cookie/cookie.py +++ b/src/secure_cookie/cookie.py @@ -280,6 +280,9 @@ def serialize(self, expires=None): if expires: self["_expires"] = _date_to_unix(expires) + return self._mac_serialize() + + def _mac_serialize(self): result = [] mac = hmac(self.secret_key, None, self.hash_method) From 4c4549bb7ca4e38c1e98cde126e1ca6b5d4a6a9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Blondon?= Date: Fri, 3 Apr 2020 18:30:24 +0200 Subject: [PATCH 3/8] use itsdangerous library to sign data --- setup.py | 2 +- src/secure_cookie/cookie.py | 53 +++++++++++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 7be04e5..6806ee1 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ from setuptools import setup # Metadata goes in setup.cfg. These are here for GitHub's dependency graph. -setup(name="secure-cookie", install_requires=["Werkzeug>0.15"]) +setup(name="secure-cookie", install_requires=["Werkzeug>0.15", "itsdangerous"]) diff --git a/src/secure_cookie/cookie.py b/src/secure_cookie/cookie.py index 9343f98..456ae09 100644 --- a/src/secure_cookie/cookie.py +++ b/src/secure_cookie/cookie.py @@ -106,6 +106,8 @@ def application(request): from numbers import Number from time import time +from itsdangerous import Signer +from itsdangerous.exc import BadSignature from werkzeug.security import safe_str_cmp from werkzeug.urls import url_quote_plus from werkzeug.urls import url_unquote_plus @@ -280,7 +282,17 @@ def serialize(self, expires=None): if expires: self["_expires"] = _date_to_unix(expires) - return self._mac_serialize() + result = [] + for key, value in sorted(self.items()): + result.append( + ( + "{}={}".format( + url_quote_plus(key), self.quote(value).decode("ascii") + ) + ).encode("ascii") + ) + signer = Signer(self.secret_key) + return signer.sign(b"&".join(result)) def _mac_serialize(self): result = [] @@ -312,7 +324,44 @@ def unserialize(cls, string, secret_key): if isinstance(secret_key, text_type): secret_key = secret_key.encode("utf-8", "replace") - return cls._mac_unserialize(string, secret_key) + signer = Signer(secret_key) + try: + serialized = signer.unsign(string) + except BadSignature: + return cls._mac_unserialize(string, secret_key) + + items = {} + for item in serialized.split(b"&"): + 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 + + if items is not None: + try: + for key, value in items.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 _mac_unserialize(cls, string, secret_key): From 18997a177249dcc4ee92310faa32cbd3a2af9432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Blondon?= Date: Fri, 3 Apr 2020 18:42:53 +0200 Subject: [PATCH 4/8] add test to check the backward compatibility for the serialization --- tests/test_cookie.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_cookie.py b/tests/test_cookie.py index 5fec8e8..6dbb4a3 100644 --- a/tests/test_cookie.py +++ b/tests/test_cookie.py @@ -51,6 +51,19 @@ def test_expire_support(): assert "x" not in c3 +def test_hmac_serialization_support(): + c = SecureCookie(secret_key=b"foo") + c["x"] = 42 + s = c._mac_serialize() + + c2 = SecureCookie.unserialize(s, b"foo") + assert c is not c2 + assert not c2.new + assert not c2.modified + assert not c2.should_save + assert c2 == c + + def test_wrapper_support(): req = Request.from_values() resp = Response() From 420707f9094f84d919db11609ce944f489e143ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Blondon?= Date: Fri, 3 Apr 2020 19:13:29 +0200 Subject: [PATCH 5/8] DeprecationWarning if the serialisation does not use itsdangerous library --- src/secure_cookie/cookie.py | 2 ++ tests/test_cookie.py | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/secure_cookie/cookie.py b/src/secure_cookie/cookie.py index 456ae09..1ecbcd9 100644 --- a/src/secure_cookie/cookie.py +++ b/src/secure_cookie/cookie.py @@ -100,6 +100,7 @@ def application(request): """ import base64 import json as _json +import warnings from datetime import datetime from hashlib import sha1 as _default_hash from hmac import new as hmac @@ -365,6 +366,7 @@ def unserialize(cls, string, secret_key): @classmethod def _mac_unserialize(cls, string, secret_key): + warnings.warn("Obsolete serialization method used", DeprecationWarning) try: base64_hash, data = string.split(b"?", 1) except (ValueError, IndexError): diff --git a/tests/test_cookie.py b/tests/test_cookie.py index 6dbb4a3..384d361 100644 --- a/tests/test_cookie.py +++ b/tests/test_cookie.py @@ -1,6 +1,7 @@ import datetime import json +import pytest from werkzeug.http import parse_cookie from werkzeug.wrappers import Request from werkzeug.wrappers import Response @@ -26,7 +27,8 @@ def test_basic_support(): assert not c2.should_save assert c2 == c - c3 = SecureCookie.unserialize(s, b"wrong foo") + with pytest.warns(DeprecationWarning): + c3 = SecureCookie.unserialize(s, b"wrong foo") assert not c3.modified assert not c3.new assert c3 == {} @@ -56,7 +58,8 @@ def test_hmac_serialization_support(): c["x"] = 42 s = c._mac_serialize() - c2 = SecureCookie.unserialize(s, b"foo") + with pytest.warns(DeprecationWarning): + c2 = SecureCookie.unserialize(s, b"foo") assert c is not c2 assert not c2.new assert not c2.modified From 388bfe5feaf8a9c99264fc26cb2142f169cbe2c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Blondon?= Date: Fri, 3 Apr 2020 19:26:54 +0200 Subject: [PATCH 6/8] use hash_method to serialize and unserialize with itsdangerous.Signer --- src/secure_cookie/cookie.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/secure_cookie/cookie.py b/src/secure_cookie/cookie.py index 1ecbcd9..c6b35a0 100644 --- a/src/secure_cookie/cookie.py +++ b/src/secure_cookie/cookie.py @@ -292,7 +292,7 @@ def serialize(self, expires=None): ) ).encode("ascii") ) - signer = Signer(self.secret_key) + signer = Signer(self.secret_key, digest_method=self.hash_method) return signer.sign(b"&".join(result)) def _mac_serialize(self): @@ -325,7 +325,7 @@ def unserialize(cls, string, secret_key): if isinstance(secret_key, text_type): secret_key = secret_key.encode("utf-8", "replace") - signer = Signer(secret_key) + signer = Signer(secret_key, digest_method=cls.hash_method) try: serialized = signer.unsign(string) except BadSignature: From 4ae5963abfa237ee977a8452af392562f0120eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Blondon?= Date: Sat, 4 Apr 2020 10:24:58 +0200 Subject: [PATCH 7/8] Deprecation message clearer. Add stacklevel parameter --- src/secure_cookie/cookie.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/secure_cookie/cookie.py b/src/secure_cookie/cookie.py index c6b35a0..f27edb5 100644 --- a/src/secure_cookie/cookie.py +++ b/src/secure_cookie/cookie.py @@ -366,7 +366,11 @@ def unserialize(cls, string, secret_key): @classmethod def _mac_unserialize(cls, string, secret_key): - warnings.warn("Obsolete serialization method used", DeprecationWarning) + warnings.warn( + "Unserializing using the old scheme. This is deprecated and the fallback will be removed in version 2.0. Ensure cookies are re-serialized using the new ItsDangerous scheme.", # noqa + DeprecationWarning, + stacklevel=3, + ) try: base64_hash, data = string.split(b"?", 1) except (ValueError, IndexError): From e1e530a48a6cc8278a69578a7334b24658bba493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Blondon?= Date: Sat, 4 Apr 2020 10:28:24 +0200 Subject: [PATCH 8/8] minimal version for itsdangerous dependency --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6806ee1..a430af8 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ from setuptools import setup # Metadata goes in setup.cfg. These are here for GitHub's dependency graph. -setup(name="secure-cookie", install_requires=["Werkzeug>0.15", "itsdangerous"]) +setup(name="secure-cookie", install_requires=["Werkzeug>0.15", "itsdangerous>=1.1"])