summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarcel Hellkamp <marc@gsites.de>2016-12-10 15:45:51 +0100
committerMarcel Hellkamp <marc@gsites.de>2016-12-10 15:45:51 +0100
commitf936dfaa7ca43551ba63d9a7b4b2a9a62ab9c0fb (patch)
tree5706413ffeb88c7e1d23bc5bf735868bbf37fa80
parent6d7e13da0f998820800ecb3fe9ccee4189aefb54 (diff)
downloadbottle-f936dfaa7ca43551ba63d9a7b4b2a9a62ab9c0fb.tar.gz
Prepare end of support for pickled-cookies (see #900)
-rwxr-xr-xbottle.py39
-rwxr-xr-xdocs/changelog.rst8
-rw-r--r--test/test_securecookies.py30
3 files changed, 46 insertions, 31 deletions
diff --git a/bottle.py b/bottle.py
index 62c00f4..8f24523 100755
--- a/bottle.py
+++ b/bottle.py
@@ -1194,15 +1194,22 @@ class BaseRequest(object):
cookies = SimpleCookie(self.environ.get('HTTP_COOKIE', '')).values()
return FormsDict((c.key, c.value) for c in cookies)
- def get_cookie(self, key, default=None, secret=None):
+ def get_cookie(self, key, default=None, secret=None, digestmod=hashlib.sha256):
""" Return the content of a cookie. To read a `Signed Cookie`, the
`secret` must match the one used to create the cookie (see
:meth:`BaseResponse.set_cookie`). If anything goes wrong (missing
cookie or wrong signature), return a default value. """
value = self.cookies.get(key)
- if secret and value:
- dec = cookie_decode(value, secret) # (key, value) tuple or None
- return dec[1] if dec and dec[0] == key else default
+ if secret:
+ # See BaseResponse.set_cookie for details on signed cookies.
+ if value and value.startswith('!') and '?' in value:
+ sig, msg = map(tob, value[1:].split('?', 1))
+ hash = hmac.new(tob(secret), msg, digestmod=digestmod).digest()
+ if _lscmp(sig, base64.b64encode(hash)):
+ dst = pickle.loads(base64.b64decode(msg))
+ if dst and dst[0] == key:
+ return dst[1]
+ return default
return value or default
@DictProperty('environ', 'bottle.request.query', read_only=True)
@@ -1774,7 +1781,7 @@ class BaseResponse(object):
return self.content_type.split('charset=')[-1].split(';')[0].strip()
return default
- def set_cookie(self, name, value, secret=None, **options):
+ def set_cookie(self, name, value, secret=None, digestmod=hashlib.sha256, **options):
""" Create a new cookie or replace an old one. If the `secret` parameter is
set, create a `Signed Cookie` (described below).
@@ -1802,6 +1809,11 @@ class BaseResponse(object):
cryptographically signed to prevent manipulation. Keep in mind that
cookies are limited to 4kb in most browsers.
+ Warning: Pickle is a potentially dangerous format. If an attacker
+ gains access to the secret key, he could forge cookies that execute
+ code on server side if unpickeld. Using pickle is discouraged and
+ support for it will be removed in later versions of bottle.
+
Warning: Signed cookies are not encrypted (the client can still see
the content) and not copy-protected (the client can restore an old
cookie). The main intention is to make pickling and unpickling
@@ -1811,9 +1823,16 @@ class BaseResponse(object):
self._cookies = SimpleCookie()
if secret:
- value = touni(cookie_encode((name, value), secret))
+ if not isinstance(value, basestring):
+ depr(0, 13, "Pickling of arbitrary objects into cookies is "
+ "deprecated.", "Only store strings in cookies. "
+ "JSON strings are fine, too.")
+ encoded = base64.b64encode(pickle.dumps([name, value], -1))
+ sig = base64.b64encode(hmac.new(tob(secret), encoded,
+ digestmod=digestmod).digest())
+ value = touni(tob('!') + sig + tob('?') + encoded)
elif not isinstance(value, basestring):
- raise TypeError('Secret key missing for non-string Cookie.')
+ raise TypeError('Secret key required for non-string cookies.')
# Cookie size plus options must not exceed 4kb.
if len(name) + len(value) > 3800:
@@ -2995,6 +3014,8 @@ def _lscmp(a, b):
def cookie_encode(data, key, digestmod=None):
""" Encode and sign a pickle-able object. Return a (byte) string """
+ depr(0, 13, "cookie_encode() will be removed soon.",
+ "Do not use this API directly.")
digestmod = digestmod or hashlib.sha256
msg = base64.b64encode(pickle.dumps(data, -1))
sig = base64.b64encode(hmac.new(tob(key), msg, digestmod=digestmod).digest())
@@ -3003,6 +3024,8 @@ def cookie_encode(data, key, digestmod=None):
def cookie_decode(data, key, digestmod=None):
""" Verify and decode an encoded string. Return an object or None."""
+ depr(0, 13, "cookie_decode() will be removed soon.",
+ "Do not use this API directly.")
data = tob(data)
if cookie_is_encoded(data):
sig, msg = data.split(tob('?'), 1)
@@ -3015,6 +3038,8 @@ def cookie_decode(data, key, digestmod=None):
def cookie_is_encoded(data):
""" Return True if the argument looks like a encoded cookie."""
+ depr(0, 13, "cookie_is_encoded() will be removed soon.",
+ "Do not use this API directly.")
return bool(data.startswith(tob('!')) and tob('?') in data)
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 2d64fa6..34f48ab 100755
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -14,8 +14,7 @@ Release 0.13
These three Python versions are no longer maintained by the Python Software Foundation and reached their end of life a long time ago. Keeping up support for ancient Python versions hinders adaptation of new features and serves no real purpose. Even Debian 7 (wheezy) and Ubuntu 12.4 (precise), both outdated, ship with Python 2.7.3 and 3.2.3 already. For this reason, we decided to drop support for Python 2.5, 2.6 and 3.1. The updated list of tested and supported python releases is as follows:
- * Python 2.7.3 ()
- * Python 2.7
+ * Python 2.7 (>= 2.7.3)
* Python 3.2
* Python 3.3
* Python 3.4
@@ -23,7 +22,7 @@ These three Python versions are no longer maintained by the Python Software Foun
* PyPy 5.3
* PyPy3 2.4
-Support for Python 2.5 was marked as deprecated since 0.12. We decided to go a step further and also remove 2.6 and 3.1 support even if it was never deprecated explicitly. This means that this release is *not* backwards compatible in Python 2.6 or 3.1 environments. Maintainers for distributions or systems that still use these old python versions should not update to Bottle 0.13 and stick with 0.12 instead.
+Support for Python 2.5 was marked as deprecated since 0.12. We decided to go a step further and also remove 2.6 and 3.1 support even if it was never deprecated explicitly in bottle. This means that this release is *not* backwards compatible in Python 2.6 or 3.1 environments. Maintainers for distributions or systems that still use these old python versions should not update to Bottle 0.13 and stick with 0.12 instead.
.. rubric:: Stabilized APIs
* The documented API of the :class:`ConfigDict` class is now considered stable and ready to use.
@@ -33,6 +32,7 @@ Support for Python 2.5 was marked as deprecated since 0.12. We decided to go a s
* :meth:`Bottle.mount` now recognizes Bottle instance and will warn about parameters that are not compatible with the new mounting behavior. The old behavior (mount applications as WSGI callable) still works and is used as a fallback automatically.
* The undocumented :func:`local_property` helper is now deprecated.
* The server adapter for google app engine is not useful anymore and marked as deprecated.
+* Bottle uses pickle to store arbitrary objects into signed cookies. This is safe, as long as the signature key remains a secret. Unfortunately, people tend to push code with signature keys to github all the time, so we decided to remove pickle-support from bottle. Signed cookies will now issue a deprecation warning if the value is not a string, and support for non-string values will be removed in 0.14. The global :func:`cookie_encode`, :func:`cookie_decode` and :func:`is_cookie_encoded` are now also deprecated. If you are using this feature, think about using json to serialize your objects before storing them into cookies, or switch to a session system that stores data server-side instead of client-side.
.. rubric:: Removed APIs (deprecated since 0.12)
* Plugins with the old API (``api=1`` or no api attribute) will no longer work.
@@ -48,7 +48,7 @@ Support for Python 2.5 was marked as deprecated since 0.12. We decided to go a s
* The magic ``{{rebase()}}`` call was replaced by a ``base`` variable. Example: ``{{base}}``
* In STPL Templates, the 'rebase' and 'include' keywords were replaced with functions in 0.12.
- * PEP-263 encoding strings are no longer recognized.
+ * PEP-263 encoding strings are no longer recognized. Templates are always utf-8.
* The 'geventSocketIO' server adapter was removed without notice. It did not work anyway.
diff --git a/test/test_securecookies.py b/test/test_securecookies.py
index 4523d36..1ade52c 100644
--- a/test/test_securecookies.py
+++ b/test/test_securecookies.py
@@ -4,26 +4,10 @@ import unittest
import bottle
from bottle import tob, touni
-class TestSecureCookies(unittest.TestCase):
- def setUp(self):
- self.data = dict(a=5, b=touni('υηι¢σ∂є'), c=[1,2,3,4,tob('bytestring')])
- self.key = tob('secret')
-
- def testDeEncode(self):
- cookie = bottle.cookie_encode(self.data, self.key)
- decoded = bottle.cookie_decode(cookie, self.key)
- self.assertEqual(self.data, decoded)
- decoded = bottle.cookie_decode(cookie+tob('x'), self.key)
- self.assertEqual(None, decoded)
- def testIsEncoded(self):
- cookie = bottle.cookie_encode(self.data, self.key)
- self.assertTrue(bottle.cookie_is_encoded(cookie))
- self.assertFalse(bottle.cookie_is_encoded(tob('some string')))
-
-class TestSecureCookiesInBottle(unittest.TestCase):
+class TestSignedCookies(unittest.TestCase):
def setUp(self):
- self.data = dict(a=5, b=touni('υηι¢σ∂є'), c=[1,2,3,4,tob('bytestring')])
+ self.data = touni('υηι¢σ∂є')
self.secret = tob('secret')
bottle.app.push()
bottle.response.bind()
@@ -36,7 +20,7 @@ class TestSecureCookiesInBottle(unittest.TestCase):
if k == 'Set-Cookie':
key, value = v.split(';')[0].split('=', 1)
yield key.lower().strip(), value.strip()
-
+
def set_pairs(self, pairs):
header = ','.join(['%s=%s' % (k, v) for k, v in pairs])
bottle.request.bind({'HTTP_COOKIE': header})
@@ -51,6 +35,12 @@ class TestSecureCookiesInBottle(unittest.TestCase):
def testWrongKey(self):
bottle.response.set_cookie('key', self.data, secret=self.secret)
pairs = self.get_pairs()
- self.set_pairs([(k+'xxx', v) for (k, v) in pairs])
+ self.set_pairs([(k + 'xxx', v) for (k, v) in pairs])
result = bottle.request.get_cookie('key', secret=self.secret)
self.assertEqual(None, result)
+
+
+class TestSignedCookiesWithPickle(TestSignedCookies):
+ def setUp(self):
+ super(TestSignedCookiesWithPickle, self).setUp()
+ self.data = dict(a=5, b=touni('υηι¢σ∂є'), c=[1,2,3,4,tob('bytestring')])