From: Ben Darnell Date: Fri, 9 Sep 2011 05:45:30 +0000 (-0700) Subject: Extract cookie-signing methods from RequestHandler so they can be used X-Git-Tag: v2.1.0~20 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=12c5699eb7d0c72698e533ae9b3d78c928f4b10f;p=thirdparty%2Ftornado.git Extract cookie-signing methods from RequestHandler so they can be used outside the web stack. Closes #339. --- diff --git a/tornado/test/web_test.py b/tornado/test/web_test.py index e6aaa59b6..ca473d899 100644 --- a/tornado/test/web_test.py +++ b/tornado/test/web_test.py @@ -3,7 +3,7 @@ from tornado.iostream import IOStream from tornado.template import DictLoader from tornado.testing import LogTrapTestCase, AsyncHTTPTestCase from tornado.util import b, bytes_type -from tornado.web import RequestHandler, _O, authenticated, Application, asynchronous, url, HTTPError, StaticFileHandler +from tornado.web import RequestHandler, _O, authenticated, Application, asynchronous, url, HTTPError, StaticFileHandler, _create_signature import binascii import logging @@ -40,13 +40,16 @@ class SecureCookieTest(LogTrapTestCase): assert match timestamp = match.group(1) sig = match.group(2) - self.assertEqual(handler._cookie_signature('foo', '12345678', - timestamp), sig) + self.assertEqual( + _create_signature(handler.application.settings["cookie_secret"], + 'foo', '12345678', timestamp), + sig) # shifting digits from payload to timestamp doesn't alter signature # (this is not desirable behavior, just confirming that that's how it # works) self.assertEqual( - handler._cookie_signature('foo', '1234', b('5678') + timestamp), + _create_signature(handler.application.settings["cookie_secret"], + 'foo', '1234', b('5678') + timestamp), sig) # tamper with the cookie handler._cookies['foo'] = utf8('1234|5678%s|%s' % (timestamp, sig)) diff --git a/tornado/web.py b/tornado/web.py index 11df300ac..59445209b 100644 --- a/tornado/web.py +++ b/tornado/web.py @@ -395,47 +395,16 @@ class RequestHandler(object): method for non-cookie uses. To decode a value not stored as a cookie use the optional value argument to get_secure_cookie. """ - timestamp = utf8(str(int(time.time()))) - value = base64.b64encode(utf8(value)) - signature = self._cookie_signature(name, value, timestamp) - value = b("|").join([value, timestamp, signature]) - return value + self.require_setting("cookie_secret", "secure cookies") + return create_signed_value(self.application.settings["cookie_secret"], + name, value) def get_secure_cookie(self, name, value=None, max_age_days=31): """Returns the given signed cookie if it validates, or None.""" - if value is None: value = self.get_cookie(name) - if not value: return None - parts = utf8(value).split(b("|")) - if len(parts) != 3: return None - signature = self._cookie_signature(name, parts[0], parts[1]) - if not _time_independent_equals(parts[2], signature): - logging.warning("Invalid cookie signature %r", value) - return None - timestamp = int(parts[1]) - if timestamp < time.time() - max_age_days * 86400: - logging.warning("Expired cookie %r", value) - return None - if timestamp > time.time() + 31 * 86400: - # _cookie_signature does not hash a delimiter between the - # parts of the cookie, so an attacker could transfer trailing - # digits from the payload to the timestamp without altering the - # signature. For backwards compatibility, sanity-check timestamp - # here instead of modifying _cookie_signature. - logging.warning("Cookie timestamp in future; possible tampering %r", value) - return None - if parts[1].startswith(b("0")): - logging.warning("Tampered cookie %r", value) - try: - return base64.b64decode(parts[0]) - except Exception: - return None - - def _cookie_signature(self, *parts): self.require_setting("cookie_secret", "secure cookies") - hash = hmac.new(utf8(self.application.settings["cookie_secret"]), - digestmod=hashlib.sha1) - for part in parts: hash.update(utf8(part)) - return utf8(hash.hexdigest()) + if value is None: value = self.get_cookie(name) + return decode_signed_value(self.application.settings["cookie_secret"], + name, value, max_age_days=max_age_days) def redirect(self, url, permanent=False): """Sends a redirect to the given (optionally relative) URL.""" @@ -1904,6 +1873,45 @@ def _time_independent_equals(a, b): result |= ord(x) ^ ord(y) return result == 0 +def create_signed_value(secret, name, value): + timestamp = utf8(str(int(time.time()))) + value = base64.b64encode(utf8(value)) + signature = _create_signature(secret, name, value, timestamp) + value = b("|").join([value, timestamp, signature]) + return value + +def decode_signed_value(secret, name, value, max_age_days=31): + if not value: return None + parts = utf8(value).split(b("|")) + if len(parts) != 3: return None + signature = _create_signature(secret, name, parts[0], parts[1]) + if not _time_independent_equals(parts[2], signature): + logging.warning("Invalid cookie signature %r", value) + return None + timestamp = int(parts[1]) + if timestamp < time.time() - max_age_days * 86400: + logging.warning("Expired cookie %r", value) + return None + if timestamp > time.time() + 31 * 86400: + # _cookie_signature does not hash a delimiter between the + # parts of the cookie, so an attacker could transfer trailing + # digits from the payload to the timestamp without altering the + # signature. For backwards compatibility, sanity-check timestamp + # here instead of modifying _cookie_signature. + logging.warning("Cookie timestamp in future; possible tampering %r", value) + return None + if parts[1].startswith(b("0")): + logging.warning("Tampered cookie %r", value) + try: + return base64.b64decode(parts[0]) + except Exception: + return None + +def _create_signature(secret, *parts): + hash = hmac.new(utf8(secret), digestmod=hashlib.sha1) + for part in parts: hash.update(utf8(part)) + return utf8(hash.hexdigest()) + class _O(dict): """Makes a dictionary behave like an object."""