GHSA-56CJ-WGG3-X943

Vulnerability from github – Published: 2026-03-10 18:30 – Updated: 2026-03-10 22:54
VLAI?
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

#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 <= 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 '\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",

static_cast(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<unsigned char>(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; }```

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/envoyproxy/envoy"
      },
      "versions": [
        "1.37.0"
      ]
    },
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/envoyproxy/envoy"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.36.0"
            },
            {
              "last_affected": "1.36.4"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/envoyproxy/envoy"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.35.0"
            },
            {
              "last_affected": "1.35.8"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Go",
        "name": "github.com/envoyproxy/envoy"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "last_affected": "1.34.12"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-26309"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-193"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-03-10T18:30:58Z",
    "nvd_published_at": "2026-03-10T20:16:35Z",
    "severity": "MODERATE"
  },
  "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, \u0027\\\\\u0027);\n  - For control characters (0x00..0x1f), it emits a JSON escape sequence of\n    length 6: \\u00XX.\n  - It uses sprintf(\u0026result[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] = \u0027\\\\\u0027; 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] = \u0027\\\\\u0027;)\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\u2019t immediately crash, it can break the std::string\n  requirement that c_str()[size()] == \u0027\\0\u0027, 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 \u003ccstdint\u003e\n  #include \u003ccstdio\u003e\n  #include \u003ccstring\u003e\n  #include \u003cstring\u003e\n  #include \u003cstring_view\u003e\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 \u0027\\\"\u0027:\n      case \u0027\\\\\u0027:\n      case \u0027\\b\u0027:\n      case \u0027\\f\u0027:\n      case \u0027\\n\u0027:\n      case \u0027\\r\u0027:\n      case \u0027\\t\u0027:\n        result += 1;\n        break;\n      default:\n        if (c == 0x00 || (c \u003e 0x00 \u0026\u0026 c \u003c= 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, \u0027\\\\\u0027);\n    uint64_t position = 0;\n\n    for (unsigned char character : input) {\n      switch (character) {\n      case \u0027\\\"\u0027:\n        result[position + 1] = \u0027\\\"\u0027;\n        position += 2;\n        break;\n      case \u0027\\\\\u0027:\n        position += 2;\n        break;\n      case \u0027\\b\u0027:\n        result[position + 1] = \u0027b\u0027;\n        position += 2;\n        break;\n      case \u0027\\f\u0027:\n        result[position + 1] = \u0027f\u0027;\n        position += 2;\n        break;\n      case \u0027\\n\u0027:\n        result[position + 1] = \u0027n\u0027;\n        position += 2;\n        break;\n      case \u0027\\r\u0027:\n        result[position + 1] = \u0027r\u0027;\n        position += 2;\n        break;\n      case \u0027\\t\u0027:\n        result[position + 1] = \u0027t\u0027;\n        position += 2;\n        break;\n      default:\n        if (character == 0x00 || (character \u003e 0x00 \u0026\u0026 character \u003c= 0x1f)) {\n          std::sprintf(\u0026result[position + 1], \"u%04x\",\n  static_cast\u003cint\u003e(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] = \u0027\\\\\u0027;\n        } else {\n          result[position++] = static_cast\u003cchar\u003e(character);\n        }\n        break;\n      }\n    }\n\n    return result;\n  }\n\n  int main() {\n    std::string input(4096, \u0027A\u0027);\n    input.push_back(\u0027\\x01\u0027); // ends with a control char -\u003e 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\u003cunsigned char\u003e(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  }```",
  "id": "GHSA-56cj-wgg3-x943",
  "modified": "2026-03-10T22:54:36Z",
  "published": "2026-03-10T18:30:58Z",
  "references": [
    {
      "type": "WEB",
      "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"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Envoy affected by off-by-one write in JsonEscaper::escapeString()"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Sightings

Author Source Type Date

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…