CVE-2026-26309: Envoy has an off-by-one write in JsonEscaper::escapeString()
Summary
An off-by-one write in Envoy::JsonEscaper::escapeString() can corrupt std::string null-termination, causing undefined behavior and potentially leading to crashes or out-of-bounds reads when the resulting string is later treated as a C-string.
### Details
The bug is in the control-character escaping path in source/common/common/ jsonescapestring.h:67.
- The function pre-sizes result to the final length: std::string result(input.size() + requiredsize, '\\'); - For control characters (0x00..0x1f), it emits a JSON escape sequence of length 6: \u00XX. - It uses sprintf(&result[position + 1], "u%04x", ...), which writes 5 chars + a trailing NUL (\0) starting at result[position + 1]. - Then it does position += 6; and writes result[position] = '\\'; to overwrite the NUL. - If the control character occurs at the end of the output (e.g., the input ends with \x01), then after position += 6, position == result.size(), so result[position] is one past the end (off-by-one), violating std::string bounds/contract.
Concretely, the problematic lines are:
- source/common/common/jsonescapestring.h:69 (sprintf(...)) - source/common/common/jsonescapestring.h:72 (result[position] = '\\';)
Potentially reachable from request-driven paths that escape untrusted data, e.g. invalid header reporting:
- source/common/http/headerutility.cc:538 ~ source/common/http/ headerutility.cc:546 (escapes invalid header key for error text)
Even when this doesn’t immediately crash, it can break the std::string requirement that cstr()[size()] == '\0', which can later trigger UB (e.g., if passed to strlen, printf("%s"), or any C API that expects NUL termination). cpp //clang++ -std=c++20 -O0 -g -fsanitize=address -fno-omit-frame-pointer reprojsonescapeasan.cc -o reprojsonescapeasan ASANOPTIONS=abortonerror=1 ./reprojsonescapeasan #include <cstdint> #include <cstdio> #include <cstring> #include <string> #include <stringview>
static uint64t extraSpace(std::stringview input) { uint64t result = 0; for (unsigned char c : input) { switch (c) { case '\"': case '\\': case '\b': case '\f': case '\n': case '\r': case '\t': result += 1; break; default: if (c == 0x00 || (c > 0x00 && c <= 0x1f)) { result += 5; } break; } } return result; }
static std::string escapeString(std::stringview input, uint64t requiredsize) { std::string result(input.size() + requiredsize, '\\'); uint64t position = 0;
for (unsigned char character : input) { switch (character) { case '\"': result[position + 1] = '\"'; position += 2; break; case '\\': position += 2; break; case '\b': result[position + 1] = 'b'; position += 2; break; case '\f': result[position + 1] = 'f'; position += 2; break; case '\n': result[position + 1] = 'n'; position += 2; break; case '\r': result[position + 1] = 'r'; position += 2; break; case '\t': result[position + 1] = 't'; position += 2; break; default: if (character == 0x00 || (character > 0x00 && character <= 0x1f)) { std::sprintf(&result[position + 1], "u%04x", staticcast<int>(character)); position += 6; // Off-by-one when this escape is the last output chunk: // position can become result.size(), so result[position] is out of bounds. result[position] = '\\'; } else { result[position++] = staticcast<char>(character); } break; } }
return result; }
int main() { std::string input(4096, 'A'); input.pushback('\x01'); // ends with a control char -> triggers the buggy path at the end
const uint64t required = extraSpace(input); std::string escaped = escapeString(input, required);
std::printf("escaped.size=%zu\n", escaped.size()); unsigned char terminator = staticcast<unsigned char>(escaped.cstr() [escaped.size()]); std::printf("escaped.cstr()[escaped.size()] = 0x%02x\n", terminator);
// If NUL termination is corrupted, this can read past the logical end. std::printf("strlen(escaped.cstr()) = %zu\n", std::strlen(escaped.cstr())); return 0; }
Other sources
Envoy is a high-performance edge/middle/service proxy. Prior to 1.37.1, 1.36.5, 1.35.8, and 1.34.13, an off-by-one write in Envoy::JsonEscaper::escapeString() can corrupt std::string null-termination, causing undefined behavior and potentially leading to crashes or out-of-bounds reads when the resulting string is later treated as a C-string. This vulnerability is fixed in 1.37.1, 1.36.5, 1.35.8, and 1.34.13.
— MITRE
Affected Software
Event History
Frequently Asked Questions
What is the severity of CVE-2026-26309?
CVE-2026-26309 has a moderate severity level due to the potential for undefined behavior and application crashes.
How do I fix CVE-2026-26309?
To mitigate CVE-2026-26309, upgrade Envoy to version 1.34.13 or later, or to versions 1.35.8 or later.
What is the impact of CVE-2026-26309?
The impact of CVE-2026-26309 includes potential crashes and out-of-bounds reads due to corrupted string null-termination.
Which Envoy versions are affected by CVE-2026-26309?
Versions of Envoy from 1.34.0 up to 1.34.12, 1.35.0 to 1.35.8, 1.36.0 to 1.36.4, and 1.37.0 are affected by CVE-2026-26309.
Is CVE-2026-26309 a critical vulnerability?
CVE-2026-26309 is not classified as critical, but it poses a risk of application instability that should be addressed.