CVE-2014-9720 (Python, published 2020-01-24T18:15Z)
Description: value: Tornado before 3.2.2 sends arbitrary responses that contain a fixed CSRF token and may be sent with HTTP compression, which makes it easier for remote attackers to conduct a BREACH attack and determine this token via a series of crafted requests.
Severity: MEDIUM (CVSS v3 base score: 6.5)
Weakness: CWE-203 \u2014 Observable Discrepancy
The product behaves differently or sends different responses under different circumstances in a way that is observable to an unauthorized actor, which exposes security-relevant information about the state of the product, such as whether a particular operation was successful or not.
Repository: https://github.com/tornadoweb/tornado
Files changed:
tornado/test/web_test.py
tornado/test/websocket_test.py
tornado/util.py
tornado/web.py
tornado/websocket.py
Commit message: Change the xsrf cookie format to be masked with a random salt.

This protects against the BREACH attack.


Vulnerable code:
            # Implementation detail: the same token is returned each time
            self.assertEqual(token, self.xsrf_token)
from tornado.websocket import WebSocketHandler, websocket_connect, WebSocketError, _websocket_mask_python
import uuid
from tornado.util import bytes_type, import_object, ObjectDict, raise_exc_info, unicode_type
            token = self.get_cookie("_xsrf")
            if not token:
                token = binascii.b2a_hex(os.urandom(16))
                self.set_cookie("_xsrf", token, expires_days=expires_days)
            self._xsrf_token = token
        if not _time_independent_equals(utf8(self.xsrf_token), utf8(token)):
import array
from tornado.util import bytes_type, unicode_type

try:
    xrange  # py2
except NameError:
    xrange = range  # py3


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()

if (os.environ.get('TORNADO_NO_EXTENSION') or
    os.environ.get('TORNADO_EXTENSION') == '0'):
    # These environment variables exist to make it easier to do performance
    # comparisons; they are not guaranteed to remain supported in the future.
    _websocket_mask = _websocket_mask_python
else:
    try:
        from tornado.speedups import websocket_mask as _websocket_mask
    except ImportError:
        if os.environ.get('TORNADO_EXTENSION') == '1':
            raise
        _websocket_mask = _websocket_mask_python

Fixed code:
        tokens_seen = set([token])
            # Tokens are encoded uniquely each time
            tokens_seen.add(token)
        self.assertEqual(len(tokens_seen), 6)
    from tornado.util import _websocket_mask_python
from tornado.websocket import WebSocketHandler, websocket_connect, WebSocketError
import array
import os
try:
    xrange  # py2
except NameError:
    xrange = range  # py3


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()

if (os.environ.get('TORNADO_NO_EXTENSION') or
    os.environ.get('TORNADO_EXTENSION') == '0'):
    # These environment variables exist to make it easier to do performance
    # comparisons; they are not guaranteed to remain supported in the future.
    _websocket_mask = _websocket_mask_python
else:
    try:
        from tornado.speedups import websocket_mask as _websocket_mask
    except ImportError:
        if os.environ.get('TORNADO_EXTENSION') == '1':
            raise
        _websocket_mask = _websocket_mask_python


from tornado.util import bytes_type, import_object, ObjectDict, raise_exc_info, unicode_type, _websocket_mask
            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:
                self.set_cookie("_xsrf", self._xsrf_token,
                                expires_days=expires_days)
    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

        _, token, _ = self._decode_xsrf_token(token)
        _, expected_token, _ = self._get_raw_xsrf_token()
        if not _time_independent_equals(utf8(token), utf8(expected_token)):
from tornado.util import bytes_type, unicode_type, _websocket_mask