{"id":"CVE-2026-26309","aliases":["BIT-envoy-2026-26309","GHSA-56cj-wgg3-x943"],"url":"https://o3.security/vulnerability/CVE-2026-26309","summary":"Envoy has an off-by-one write in JsonEscaper::escapeString()","details":"### Summary\n\n  An off-by-one write in Envoy::JsonEscaper::escapeString() can corrupt\n  std::string null-termination, causing undefined behavior and potentially\n  leading to crashes or out-of-bounds reads when the resulting string is later\n  treated as a C-string.\n\n  ### Details\n\n  The bug is in the control-character escaping path in source/common/common/\n  json_escape_string.h:67.\n\n  - The function pre-sizes result to the final length: std::string\n    result(input.size() + required_size, '\\\\');\n  - For control characters (0x00..0x1f), it emits a JSON escape sequence of\n    length 6: \\u00XX.\n  - It uses sprintf(&result[position + 1], \"u%04x\", ...), which writes 5 chars +\n    a trailing NUL (\\0) starting at result[position + 1].\n  - Then it does position += 6; and writes result[position] = '\\\\'; to overwrite\n    the NUL.\n  - If the control character occurs at the end of the output (e.g., the input\n    ends with \\x01), then after position += 6, position == result.size(), so\n    result[position] is one past the end (off-by-one), violating std::string\n    bounds/contract.\n\n  Concretely, the problematic lines are:\n\n  - source/common/common/json_escape_string.h:69 (sprintf(...))\n  - source/common/common/json_escape_string.h:72 (result[position] = '\\\\';)\n\n  Potentially reachable from request-driven paths that escape untrusted data,\n  e.g. invalid header reporting:\n\n  - source/common/http/header_utility.cc:538 ~ source/common/http/\n    header_utility.cc:546 (escapes invalid header key for error text)\n\n  Even when this doesn’t immediately crash, it can break the std::string\n  requirement that c_str()[size()] == '\\0', which can later trigger UB (e.g., if\n  passed to strlen, printf(\"%s\"), or any C API that expects NUL termination).\n  \n  \n  ```cpp\n//clang++ -std=c++20 -O0 -g -fsanitize=address -fno-omit-frame-pointer\n  repro_json_escape_asan.cc -o repro_json_escape_asan\n  ASAN_OPTIONS=abort_on_error=1 ./repro_json_escape_asan\n#include <cstdint>\n  #include <cstdio>\n  #include <cstring>\n  #include <string>\n  #include <string_view>\n\n  static uint64_t extraSpace(std::string_view input) {\n    uint64_t result = 0;\n    for (unsigned char c : input) {\n      switch (c) {\n      case '\\\"':\n      case '\\\\':\n      case '\\b':\n      case '\\f':\n      case '\\n':\n      case '\\r':\n      case '\\t':\n        result += 1;\n        break;\n      default:\n        if (c == 0x00 || (c > 0x00 && c <= 0x1f)) {\n          result += 5;\n        }\n        break;\n      }\n    }\n    return result;\n  }\n\n  static std::string escapeString(std::string_view input, uint64_t\n  required_size) {\n    std::string result(input.size() + required_size, '\\\\');\n    uint64_t position = 0;\n\n    for (unsigned char character : input) {\n      switch (character) {\n      case '\\\"':\n        result[position + 1] = '\\\"';\n        position += 2;\n        break;\n      case '\\\\':\n        position += 2;\n        break;\n      case '\\b':\n        result[position + 1] = 'b';\n        position += 2;\n        break;\n      case '\\f':\n        result[position + 1] = 'f';\n        position += 2;\n        break;\n      case '\\n':\n        result[position + 1] = 'n';\n        position += 2;\n        break;\n      case '\\r':\n        result[position + 1] = 'r';\n        position += 2;\n        break;\n      case '\\t':\n        result[position + 1] = 't';\n        position += 2;\n        break;\n      default:\n        if (character == 0x00 || (character > 0x00 && character <= 0x1f)) {\n          std::sprintf(&result[position + 1], \"u%04x\",\n  static_cast<int>(character));\n          position += 6;\n          // Off-by-one when this escape is the last output chunk:\n          // position can become result.size(), so result[position] is out of\n  bounds.\n          result[position] = '\\\\';\n        } else {\n          result[position++] = static_cast<char>(character);\n        }\n        break;\n      }\n    }\n\n    return result;\n  }\n\n  int main() {\n    std::string input(4096, 'A');\n    input.push_back('\\x01'); // ends with a control char -> triggers the buggy\n  path at the end\n\n    const uint64_t required = extraSpace(input);\n    std::string escaped = escapeString(input, required);\n\n    std::printf(\"escaped.size=%zu\\n\", escaped.size());\n    unsigned char terminator = static_cast<unsigned char>(escaped.c_str()\n  [escaped.size()]);\n    std::printf(\"escaped.c_str()[escaped.size()] = 0x%02x\\n\", terminator);\n\n    // If NUL termination is corrupted, this can read past the logical end.\n    std::printf(\"strlen(escaped.c_str()) = %zu\\n\",\n  std::strlen(escaped.c_str()));\n    return 0;\n  }```","published":"2026-03-10T19:04:21.384Z","modified":"2026-08-12T03:51:35.348512753Z","cvss":{"score":5.3,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L"},"epss":{"score":0.00365,"percentile":0.3017,"asOf":"2026-09-17"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Go","name":"github.com/envoyproxy/envoy","fixedVersion":null},{"ecosystem":"Go","name":"github.com/envoyproxy/envoy","fixedVersion":null},{"ecosystem":"Go","name":"github.com/envoyproxy/envoy","fixedVersion":null},{"ecosystem":"Go","name":"github.com/envoyproxy/envoy","fixedVersion":null}],"fix":null,"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/26xxx/CVE-2026-26309.json"},{"type":"ADVISORY","url":"https://github.com/envoyproxy/envoy/security/advisories/GHSA-56cj-wgg3-x943"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-26309"},{"type":"PACKAGE","url":"https://github.com/envoyproxy/envoy"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:35.348512753Z"}}