VYPR
Low severity3.7GHSA Advisory· Published Jun 12, 2026· Updated Jun 12, 2026

Tornado has out-of-bounds memory access via C extension

CVE-2026-49854

Description

Tornado's C extension reads out-of-bounds memory in websocket_mask when mask argument is shorter than 4 bytes, fixed in v6.5.6.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Tornado's C extension reads out-of-bounds memory in websocket_mask when mask argument is shorter than 4 bytes, fixed in v6.5.6.

Vulnerability

Tornado's optional native extension tornado.speedups implements websocket_mask without validating that the mask argument is exactly four bytes long. The C function unconditionally reads four bytes from mask, even when Python passes a shorter byte string, causing an out-of-bounds read of up to three bytes beyond the provided buffer [2][3]. The vulnerable code path is reachable from Tornado's XSRF token decoder when xsrf_cookies=True and the native extension is active [2]. All versions prior to Tornado 6.5.6 that use the native extension are affected [1].

Exploitation

An attacker can trigger the out-of-bounds read by sending a crafted request that causes the XSRF token decoder to invoke websocket_mask with a mask byte string shorter than four bytes. The attacker does not need authentication; the XSRF token processing occurs on any request with xsrf_cookies=True. The native extension is enabled by default, but the environment variable TORNADO_EXTENSION=0 can disable it [1]. The read is bounded to up to three bytes past the original buffer, and the attacker has no control over which memory is exposed [2][3].

Impact

The vulnerability allows an unauthenticated attacker to read up to three bytes of uninitialized memory from the heap. This information disclosure could leak sensitive data such as secrets or other application memory, though the impact is limited due to the small and random nature of the leak. No write or code execution is possible [2][3].

Mitigation

The bug is fixed in Tornado 6.5.6, released on May 27, 2026 [1]. Users unable to upgrade immediately can set the environment variable TORNADO_EXTENSION=0 to disable the native extension, which eliminates the vulnerability at the cost of reduced websocket performance [2][3]. No other workarounds are available.

AI Insight generated on Jun 12, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

1

Patches

1
96dc88c2a057

speedups: validate mask length

https://github.com/tornadoweb/tornadoBen DarnellMay 27, 2026Fixed in 6.5.6via llm-release-walk
3 files changed · +50 28
  • tornado/speedups.c+40 27 modified
    @@ -2,63 +2,76 @@
     #include <Python.h>
     #include <stdint.h>
     
    -static PyObject* websocket_mask(PyObject* self, PyObject* args) {
    -    const char* mask;
    +static PyObject *websocket_mask(PyObject *self, PyObject *args)
    +{
    +    const char *mask;
         Py_ssize_t mask_len;
         uint32_t uint32_mask;
         uint64_t uint64_mask;
    -    const char* data;
    +    const char *data;
         Py_ssize_t data_len;
         Py_ssize_t i;
    -    PyObject* result;
    -    char* buf;
    +    PyObject *result;
    +    char *buf;
     
    -    if (!PyArg_ParseTuple(args, "s#s#", &mask, &mask_len, &data, &data_len)) {
    +    if (!PyArg_ParseTuple(args, "s#s#", &mask, &mask_len, &data, &data_len))
    +    {
             return NULL;
         }
     
    -    uint32_mask = ((uint32_t*)mask)[0];
    +    if (mask_len != 4)
    +    {
    +        PyErr_SetString(PyExc_ValueError, "mask must be 4 bytes");
    +        return NULL;
    +    }
    +
    +    uint32_mask = ((uint32_t *)mask)[0];
     
         result = PyBytes_FromStringAndSize(NULL, data_len);
    -    if (!result) {
    +    if (!result)
    +    {
             return NULL;
         }
         buf = PyBytes_AsString(result);
     
    -    if (sizeof(size_t) >= 8) {
    +    if (sizeof(size_t) >= 8)
    +    {
             uint64_mask = uint32_mask;
             uint64_mask = (uint64_mask << 32) | uint32_mask;
     
    -        while (data_len >= 8) {
    -            ((uint64_t*)buf)[0] = ((uint64_t*)data)[0] ^ uint64_mask;
    +        while (data_len >= 8)
    +        {
    +            ((uint64_t *)buf)[0] = ((uint64_t *)data)[0] ^ uint64_mask;
                 data += 8;
                 buf += 8;
                 data_len -= 8;
             }
         }
     
    -    while (data_len >= 4) {
    -        ((uint32_t*)buf)[0] = ((uint32_t*)data)[0] ^ uint32_mask;
    +    while (data_len >= 4)
    +    {
    +        ((uint32_t *)buf)[0] = ((uint32_t *)data)[0] ^ uint32_mask;
             data += 4;
             buf += 4;
             data_len -= 4;
         }
     
    -    for (i = 0; i < data_len; i++) {
    +    for (i = 0; i < data_len; i++)
    +    {
             buf[i] = data[i] ^ mask[i];
         }
     
         return result;
     }
     
    -static int speedups_exec(PyObject *module) {
    +static int speedups_exec(PyObject *module)
    +{
         return 0;
     }
     
     static PyMethodDef methods[] = {
    -    {"websocket_mask",  websocket_mask, METH_VARARGS, ""},
    -    {NULL, NULL, 0, NULL}
    -};
    +    {"websocket_mask", websocket_mask, METH_VARARGS, ""},
    +    {NULL, NULL, 0, NULL}};
     
     static PyModuleDef_Slot slots[] = {
         {Py_mod_exec, speedups_exec},
    @@ -68,19 +81,19 @@ static PyModuleDef_Slot slots[] = {
     #if (!defined(Py_LIMITED_API) && PY_VERSION_HEX >= 0x030d0000) || Py_LIMITED_API >= 0x030d0000
         {Py_mod_gil, Py_MOD_GIL_NOT_USED},
     #endif
    -    {0, NULL}
    -};
    +    {0, NULL}};
     
     static struct PyModuleDef speedupsmodule = {
    -   PyModuleDef_HEAD_INIT,
    -   "speedups",
    -   NULL,
    -   0,
    -   methods,
    -   slots,
    +    PyModuleDef_HEAD_INIT,
    +    "speedups",
    +    NULL,
    +    0,
    +    methods,
    +    slots,
     };
     
     PyMODINIT_FUNC
    -PyInit_speedups(void) {
    +PyInit_speedups(void)
    +{
         return PyModuleDef_Init(&speedupsmodule);
     }
    
  • tornado/test/websocket_test.py+7 0 modified
    @@ -794,6 +794,13 @@ def test_mask(self: typing.Any):
                 b"\xff\xfa\xff\xff\xfb\xfe",
             )
     
    +    def test_length_validation(self: typing.Any):
    +        # Test all lengths of mask that are not 4 bytes.
    +        for mask in (b"", b"a", b"ab", b"abc", b"abcde", b"abcdef"):
    +            with self.subTest(mask=mask):
    +                with self.assertRaises(ValueError):
    +                    self.mask(mask, b"data asdf")
    +
     
     class PythonMaskFunctionTest(MaskFunctionMixin):
         def mask(self, mask, data):
    
  • tornado/util.py+3 1 modified
    @@ -145,7 +145,7 @@ def exec_in(
     
     
     def raise_exc_info(
    -    exc_info: Tuple[Optional[type], Optional[BaseException], Optional["TracebackType"]]
    +    exc_info: Tuple[Optional[type], Optional[BaseException], Optional["TracebackType"]],
     ) -> typing.NoReturn:
         try:
             if exc_info[1] is not None:
    @@ -418,6 +418,8 @@ def _websocket_mask_python(mask: bytes, data: bytes) -> bytes:
     
         This pure-python implementation may be replaced by an optimized version when available.
         """
    +    if len(mask) != 4:
    +        raise ValueError("mask must be 4 bytes")
         mask_arr = array.array("B", mask)
         unmasked_arr = array.array("B", data)
         for i in range(len(data)):
    

Vulnerability mechanics

Root cause

"Missing input validation on mask length in the C extension's websocket_mask function allows an out-of-bounds read of up to 3 bytes."

Attack vector

An attacker can trigger the bug by supplying a `mask` byte string shorter than 4 bytes to any code path that calls `websocket_mask` with attacker-controlled input. The advisory notes that Tornado's XSRF token decoder reaches this function when `xsrf_cookies=True` and the native extension is active. The C function unconditionally casts the mask pointer to `uint32_t*` and dereferences it, reading beyond the provided buffer and exposing up to 3 bytes of uninitialized memory. No authentication is required if the attacker can craft a request that includes a short mask value.

Affected code

The vulnerability resides in `tornado/speedups.c` in the `websocket_mask` function. The C extension reads four bytes from the `mask` argument via `((uint32_t*)mask)[0]` without first verifying that `mask_len` equals 4, allowing an out-of-bounds read of up to three bytes. The Python fallback in `tornado/util.py` also lacked the length check before the patch.

What the fix does

The patch adds an explicit length check at the top of `websocket_mask` in `speedups.c`: if `mask_len != 4`, it raises a `ValueError` with the message "mask must be 4 bytes" before any memory access occurs. The same guard is added to the pure-Python fallback in `tornado/util.py`. This ensures that only a properly-sized 4-byte mask is ever dereferenced, closing the out-of-bounds read.

Preconditions

  • configThe native extension `tornado.speedups` must be active (not disabled via `TORNADO_EXTENSION=0`).
  • inputThe attacker must be able to supply a `mask` value shorter than 4 bytes to a code path that invokes `websocket_mask`.

Generated on Jun 12, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

3

News mentions

0

No linked articles in our index yet.