GitHub Security Advisory: GHSA-56cj-wgg3-x943
Package: github.com/envoyproxy/envoy (GO)
Severity: MODERATE (CVSS 5.3)
CWE: CWE-193: Off-by-one Error
Vulnerable versions: >= 1.36.0, <= 1.36.4
Patched in: unpatched

Summary: Envoy affected by off-by-one write in JsonEscaper::escapeString()

Details:
### 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 '\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::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':