GitHub Advisoryadvisory · Go
GHSA-56cj-wgg3-x943 Envoy affected by off-by-one write in JsonEscaper::escapeString()
MEDIUM
Official page ↗
- Aliases
- CVE-2026-26309
- Ecosystem
- Go
- Packages
- github.com/envoyproxy/envoy
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
- Weaknesses
- CWE-193
- Reviewed
- 2026-03-10
Affected packages
• Go github.com/envoyproxy/envoy: 1.37.0 (no fix yet)
• Go github.com/envoyproxy/envoy: >= 1.36.0, <= 1.36.4 (no fix yet)
• Go github.com/envoyproxy/envoy: >= 1.35.0, <= 1.35.8 (no fix yet)
• Go github.com/envoyproxy/envoy: <= 1.34.12 (no fix yet)
Description
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/
json_escape_string.h:67.
• The function pre-sizes result to the final length: std::string
result(input.size() + required_size, '\\');
• 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/json_escape_string.h:69 (sprintf(...))
• source/common/common/json_escape_string.h:72 (result[position] = '\\';)
Potentially reachable from request-driven paths that escape untrusted data,
e.g. invalid header reporting:
• source/common/http/header_utility.cc:538 ~ source/common/http/
header_utility.cc:546 (escapes invalid header key for error text)
Even when this doesn’t immediately crash, it can break the std::string
requirement that c_str()[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
repro_json_escape_asan.cc -o repro_json_escape_asan
ASAN_OPTIONS=abort_on_error=1 ./repro_json_escape_asan
include
#include
#include
#include
#include
static uint64_t extraSpace(std::string_view input) {
uint64_t 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 0x00 && character (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++] = static_cast(character);
}
break;
}
}
return result;
}
int main() {
std::string input(4096, 'A');
input.push_back('\x01'); // ends with a control char -> triggers the buggy
path at the end
const uint64_t required = extraSpace(input);
std::string escaped = escapeString(input, required);
std::printf("escaped.size=%zu\n", escaped.size());
unsigned char terminator = static_cast(escaped.c_str()
[escaped.size()]);
std::printf("escaped.c_str()[escaped.size()] = 0x%02x\n", terminator);
// If NUL termination is corrupted, this can read past the logical end.
std::printf("strlen(escaped.c_str()) = %zu\n",
std::strlen(escaped.c_str()));
return 0;
}```
Related
Alias · CVE (1)
- CVE-2026-26309 Envoy is a high-performance edge/middle/service proxy. vulnerability
Weakness · CWE (1)
- CWE-193 Off-by-one Error weakness