1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123
|
Description: Mask xsrf cookie format with random salt - BREECH protection
* Address CVE-2014-9720 by adapting upstream fix to the 1.0.1 code base
- Upstream commit 1c36307463b1e8affae100bf9386948e6c1b2308
Author: Scott Kitterman <scott@kitterman.com>
Origin: https://github.com/tornadoweb/tornado/commit/1c36307463b1e8affae100bf9386948e6c1b2308?diff=unified
Forwarded: not-needed
Last-Update: 2015-07-22
--- python-tornado-1.0.1.orig/tornado/web.py
+++ python-tornado-1.0.1/tornado/web.py
@@ -43,6 +43,7 @@ See the Tornado walkthrough on GitHub fo
getting started guide.
"""
+import array
import base64
import binascii
import calendar
@@ -59,6 +60,7 @@ import httplib
import locale
import logging
import mimetypes
+import os
import os.path
import re
import stat
@@ -658,13 +660,55 @@ class RequestHandler(object):
See http://en.wikipedia.org/wiki/Cross-site_request_forgery
"""
if not hasattr(self, "_xsrf_token"):
- token = self.get_cookie("_xsrf")
- if not token:
- token = binascii.b2a_hex(uuid.uuid4().bytes)
+ version, token, timestamp = self._get_raw_xsrf_token()
+ mask = os.urandom(4)
+ self._xsrf_token = b"|".join([
+ b"2",
+ binascii.b2a_hex(mask),
+ binascii.b2a_hex(_websocket_mask(mask, token)),
+ utf8(str(int(timestamp)))])
+ if version is None or version != 2:
expires_days = 30 if self.current_user else None
- self.set_cookie("_xsrf", token, expires_days=expires_days)
- self._xsrf_token = token
+ self.set_cookie("_xsrf", self._xsrf_token,
+ expires_days=expires_days)
return self._xsrf_token
+
+ def _get_raw_xsrf_token(self):
+ if not hasattr(self, '_raw_xsrf_token'):
+ cookie = self.get_cookie("_xsrf")
+ if cookie:
+ version, token, timestamp = self._decode_xsrf_token(cookie)
+ else:
+ version, token, timestamp = None, None, None
+ if token is None:
+ version = None
+ token = os.urandom(16)
+ timestamp = time.time()
+ self._raw_xsrf_token = (version, token, timestamp)
+ return self._raw_xsrf_token
+
+ def _decode_xsrf_token(self, cookie):
+ m = _signed_value_version_re.match(utf8(cookie))
+ if m:
+ version = int(m.group(1))
+ if version == 2:
+ _, mask, masked_token, timestamp = cookie.split("|")
+ mask = binascii.a2b_hex(utf8(mask))
+ token = _websocket_mask(
+ mask, binascii.a2b_hex(utf8(masked_token)))
+ timestamp = int(timestamp)
+ return version, token, timestamp
+ else:
+ # Treat unknown versions as not present instead of failing.
+ return None, None, None
+ elif len(cookie) == 32:
+ version = 1
+ token = binascii.a2b_hex(cookie)
+ # We don't have a usable timestamp in older versions.
+ timestamp = int(time.time())
+ return (version, token, timestamp)
+ else:
+ return None, None, None
def check_xsrf_cookie(self):
"""Verifies that the '_xsrf' cookie matches the '_xsrf' argument.
@@ -681,7 +725,9 @@ class RequestHandler(object):
token = self.get_argument("_xsrf", None)
if not token:
raise HTTPError(403, "'_xsrf' argument missing from POST")
- if self.xsrf_token != token:
+ _, token, _ = self._decode_xsrf_token(token)
+ _, expected_token, _ = self._get_raw_xsrf_token()
+ if not _time_independent_equals(utf8(token), utf8(expected_token)):
raise HTTPError(403, "XSRF cookie does not match POST argument")
def xsrf_form_html(self):
@@ -1501,3 +1547,24 @@ class _O(dict):
def __setattr__(self, name, value):
self[name] = value
+
+def _websocket_mask_python(mask, data):
+ """Websocket masking function.
+
+ `mask` is a `bytes` object of length 4; `data` is a `bytes` object of any length.
+ Returns a `bytes` object of the same length as `data` with the mask applied
+ as specified in section 5.3 of RFC 6455.
+
+ This pure-python implementation may be replaced by an optimized version when available.
+ """
+ mask = array.array("B", mask)
+ unmasked = array.array("B", data)
+ for i in xrange(len(data)):
+ unmasked[i] = unmasked[i] ^ mask[i % 4]
+ if hasattr(unmasked, 'tobytes'):
+ # tostring was deprecated in py32. It hasn't been removed,
+ # but since we turn on deprecation warnings in our tests
+ # we need to use the right one.
+ return unmasked.tobytes()
+ else:
+ return unmasked.tostring()
|