Skip to content

Commit 12c5699

Browse files
committed
Extract cookie-signing methods from RequestHandler so they can be used
outside the web stack. Closes tornadoweb#339.
1 parent c9fb8e0 commit 12c5699

File tree

2 files changed

+52
-41
lines changed

2 files changed

+52
-41
lines changed

tornado/test/web_test.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from tornado.template import DictLoader
44
from tornado.testing import LogTrapTestCase, AsyncHTTPTestCase
55
from tornado.util import b, bytes_type
6-
from tornado.web import RequestHandler, _O, authenticated, Application, asynchronous, url, HTTPError, StaticFileHandler
6+
from tornado.web import RequestHandler, _O, authenticated, Application, asynchronous, url, HTTPError, StaticFileHandler, _create_signature
77

88
import binascii
99
import logging
@@ -40,13 +40,16 @@ def test_cookie_tampering_future_timestamp(self):
4040
assert match
4141
timestamp = match.group(1)
4242
sig = match.group(2)
43-
self.assertEqual(handler._cookie_signature('foo', '12345678',
44-
timestamp), sig)
43+
self.assertEqual(
44+
_create_signature(handler.application.settings["cookie_secret"],
45+
'foo', '12345678', timestamp),
46+
sig)
4547
# shifting digits from payload to timestamp doesn't alter signature
4648
# (this is not desirable behavior, just confirming that that's how it
4749
# works)
4850
self.assertEqual(
49-
handler._cookie_signature('foo', '1234', b('5678') + timestamp),
51+
_create_signature(handler.application.settings["cookie_secret"],
52+
'foo', '1234', b('5678') + timestamp),
5053
sig)
5154
# tamper with the cookie
5255
handler._cookies['foo'] = utf8('1234|5678%s|%s' % (timestamp, sig))

tornado/web.py

Lines changed: 45 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -395,47 +395,16 @@ def create_signed_value(self, name, value):
395395
method for non-cookie uses. To decode a value not stored
396396
as a cookie use the optional value argument to get_secure_cookie.
397397
"""
398-
timestamp = utf8(str(int(time.time())))
399-
value = base64.b64encode(utf8(value))
400-
signature = self._cookie_signature(name, value, timestamp)
401-
value = b("|").join([value, timestamp, signature])
402-
return value
398+
self.require_setting("cookie_secret", "secure cookies")
399+
return create_signed_value(self.application.settings["cookie_secret"],
400+
name, value)
403401

404402
def get_secure_cookie(self, name, value=None, max_age_days=31):
405403
"""Returns the given signed cookie if it validates, or None."""
406-
if value is None: value = self.get_cookie(name)
407-
if not value: return None
408-
parts = utf8(value).split(b("|"))
409-
if len(parts) != 3: return None
410-
signature = self._cookie_signature(name, parts[0], parts[1])
411-
if not _time_independent_equals(parts[2], signature):
412-
logging.warning("Invalid cookie signature %r", value)
413-
return None
414-
timestamp = int(parts[1])
415-
if timestamp < time.time() - max_age_days * 86400:
416-
logging.warning("Expired cookie %r", value)
417-
return None
418-
if timestamp > time.time() + 31 * 86400:
419-
# _cookie_signature does not hash a delimiter between the
420-
# parts of the cookie, so an attacker could transfer trailing
421-
# digits from the payload to the timestamp without altering the
422-
# signature. For backwards compatibility, sanity-check timestamp
423-
# here instead of modifying _cookie_signature.
424-
logging.warning("Cookie timestamp in future; possible tampering %r", value)
425-
return None
426-
if parts[1].startswith(b("0")):
427-
logging.warning("Tampered cookie %r", value)
428-
try:
429-
return base64.b64decode(parts[0])
430-
except Exception:
431-
return None
432-
433-
def _cookie_signature(self, *parts):
434404
self.require_setting("cookie_secret", "secure cookies")
435-
hash = hmac.new(utf8(self.application.settings["cookie_secret"]),
436-
digestmod=hashlib.sha1)
437-
for part in parts: hash.update(utf8(part))
438-
return utf8(hash.hexdigest())
405+
if value is None: value = self.get_cookie(name)
406+
return decode_signed_value(self.application.settings["cookie_secret"],
407+
name, value, max_age_days=max_age_days)
439408

440409
def redirect(self, url, permanent=False):
441410
"""Sends a redirect to the given (optionally relative) URL."""
@@ -1904,6 +1873,45 @@ def _time_independent_equals(a, b):
19041873
result |= ord(x) ^ ord(y)
19051874
return result == 0
19061875

1876+
def create_signed_value(secret, name, value):
1877+
timestamp = utf8(str(int(time.time())))
1878+
value = base64.b64encode(utf8(value))
1879+
signature = _create_signature(secret, name, value, timestamp)
1880+
value = b("|").join([value, timestamp, signature])
1881+
return value
1882+
1883+
def decode_signed_value(secret, name, value, max_age_days=31):
1884+
if not value: return None
1885+
parts = utf8(value).split(b("|"))
1886+
if len(parts) != 3: return None
1887+
signature = _create_signature(secret, name, parts[0], parts[1])
1888+
if not _time_independent_equals(parts[2], signature):
1889+
logging.warning("Invalid cookie signature %r", value)
1890+
return None
1891+
timestamp = int(parts[1])
1892+
if timestamp < time.time() - max_age_days * 86400:
1893+
logging.warning("Expired cookie %r", value)
1894+
return None
1895+
if timestamp > time.time() + 31 * 86400:
1896+
# _cookie_signature does not hash a delimiter between the
1897+
# parts of the cookie, so an attacker could transfer trailing
1898+
# digits from the payload to the timestamp without altering the
1899+
# signature. For backwards compatibility, sanity-check timestamp
1900+
# here instead of modifying _cookie_signature.
1901+
logging.warning("Cookie timestamp in future; possible tampering %r", value)
1902+
return None
1903+
if parts[1].startswith(b("0")):
1904+
logging.warning("Tampered cookie %r", value)
1905+
try:
1906+
return base64.b64decode(parts[0])
1907+
except Exception:
1908+
return None
1909+
1910+
def _create_signature(secret, *parts):
1911+
hash = hmac.new(utf8(secret), digestmod=hashlib.sha1)
1912+
for part in parts: hash.update(utf8(part))
1913+
return utf8(hash.hexdigest())
1914+
19071915

19081916
class _O(dict):
19091917
"""Makes a dictionary behave like an object."""

0 commit comments

Comments
 (0)