PT-2026-24617 · Go · Github.Com/Envoyproxy/Envoy
Published
2026-03-10
·
Updated
2026-03-10
CVSS v3.1
5.3
Medium
| Vector | AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L |
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 <cstdint>
#include <cstdio>
#include <cstring>
#include <string>
#include <string view>
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 '
':
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::string view input, uint64 t
required size) {
std::string result(input.size() + required size, '');
uint64 t 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 '
':
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",
static cast<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++] = static cast<char>(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
", escaped.size());
unsigned char terminator = static cast<unsigned char>(escaped.c str()
[escaped.size()]);
std::printf("escaped.c str()[escaped.size()] = 0x%02x
", terminator);
// If NUL termination is corrupted, this can read past the logical end.
std::printf("strlen(escaped.c str()) = %zu
",
std::strlen(escaped.c str()));
return 0;
}```Fix
Found an issue in the description? Have something to add? Feel free to write us 👾
Weakness Enumeration
Related Identifiers
Affected Products
Github.Com/Envoyproxy/Envoy