{"id":"CVE-2026-26309","aliases":["BIT-envoy-2026-26309","EUVD-2026-10800","GHSA-56cj-wgg3-x943"],"status":"published","title":"Envoy has an off-by-one write in JsonEscaper::escapeString()","description":"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.","severity":{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-11","score":0.00414,"percentile":0.36219,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-193"],"published":"2026-03-10T18:30:58Z","modified":"2026-10-28T02:30:49Z","fixes":[{"ecosystem":"Bitnami","package":"envoy","fixed_in":["1.34.13","1.35.9","1.36.5","1.37.1"],"sources":["osv"]}],"affected":[{"source":"ghsa","ecosystem":"Go","package":"github.com/envoyproxy/envoy","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"1.37.0"},{"last_affected":"1.37.0"}]}],"versions":["1.37.0"]},{"source":"ghsa","ecosystem":"Go","package":"github.com/envoyproxy/envoy","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"1.36.0"},{"last_affected":"1.36.4"}]}]},{"source":"ghsa","ecosystem":"Go","package":"github.com/envoyproxy/envoy","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"1.35.0"},{"last_affected":"1.35.8"}]}]},{"source":"ghsa","ecosystem":"Go","package":"github.com/envoyproxy/envoy","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"1.34.12"}]}]},{"source":"osv","ecosystem":"Bitnami","package":"envoy","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"1.34.13"},{"introduced":"1.35.0"},{"fixed":"1.35.9"},{"introduced":"1.36.0"},{"fixed":"1.36.5"},{"introduced":"1.37.0"},{"fixed":"1.37.1"}]}]},{"source":"osv","ecosystem":"Go","package":"github.com/envoyproxy/envoy","versions":["1.37.0"]},{"source":"osv","ecosystem":"Go","package":"github.com/envoyproxy/envoy","ranges":[{"type":"SEMVER","events":[{"introduced":"1.36.0"},{"last_affected":"1.36.4"}]}]},{"source":"osv","ecosystem":"Go","package":"github.com/envoyproxy/envoy","ranges":[{"type":"SEMVER","events":[{"introduced":"1.35.0"},{"last_affected":"1.35.8"}]}]},{"source":"osv","ecosystem":"Go","package":"github.com/envoyproxy/envoy","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"last_affected":"1.34.12"}]}]}],"products":[{"vendor":"envoyproxy","product":"envoy"}],"references":[{"url":"https://github.com/envoyproxy/envoy/security/advisories/GHSA-56cj-wgg3-x943","tags":["x_refsource_CONFIRM","Exploit","Vendor Advisory","advisory","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-26309","tags":["web","advisory"]},{"url":"https://github.com/advisories/GHSA-56cj-wgg3-x943"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/26xxx/CVE-2026-26309.json","tags":["advisory"]},{"url":"https://github.com/envoyproxy/envoy","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-26309","title":"Envoy has an off-by-one write in JsonEscaper::escapeString()","description":"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.","status":"published","published":"2026-03-10T19:04:21.384Z","modified":"2026-03-10T20:12:40.483Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L","level":"medium","assessor":"cna"}],"cwes":["CWE-193"],"updated_at":"2026-11-02T13:38:20.971157Z"}],"euvd":[{"id":"EUVD-2026-10800","description":"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.","status":"published","published":"2026-03-10T19:04:21Z","modified":"2026-03-10T20:12:40Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L","level":"medium"}],"updated_at":"2026-11-02T19:48:04.718931Z"}],"ghsa":[{"id":"GHSA-56cj-wgg3-x943","title":"Envoy affected by off-by-one write in JsonEscaper::escapeString()","description":"### 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, '\\\\');\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] = '\\\\'; 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] = '\\\\';)\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’t immediately crash, it can break the std::string\n  requirement that c_str()[size()] == '\\0', 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 '\\\"':\n      case '\\\\':\n      case '\\b':\n      case '\\f':\n      case '\\n':\n      case '\\r':\n      case '\\t':\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, '\\\\');\n    uint64_t position = 0;\n\n    for (unsigned char character : input) {\n      switch (character) {\n      case '\\\"':\n        result[position + 1] = '\\\"';\n        position += 2;\n        break;\n      case '\\\\':\n        position += 2;\n        break;\n      case '\\b':\n        result[position + 1] = 'b';\n        position += 2;\n        break;\n      case '\\f':\n        result[position + 1] = 'f';\n        position += 2;\n        break;\n      case '\\n':\n        result[position + 1] = 'n';\n        position += 2;\n        break;\n      case '\\r':\n        result[position + 1] = 'r';\n        position += 2;\n        break;\n      case '\\t':\n        result[position + 1] = 't';\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] = '\\\\';\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, 'A');\n    input.push_back('\\x01'); // 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  }```","status":"published","published":"2026-03-10T18:30:58Z","modified":"2026-03-10T22:54:36Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L","level":"medium"}],"cwes":["CWE-193"],"updated_at":"2026-11-02T14:39:33.538417Z"}],"nvd":[{"id":"CVE-2026-26309","description":"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.","status":"published","published":"2026-03-10T20:16:35.88Z","modified":"2026-06-17T10:26:03.94Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L","level":"medium","assessor":"secondary"},{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L","level":"medium","assessor":"primary"}],"cwes":["CWE-193"],"updated_at":"2026-11-02T14:12:08.98071Z"}],"osv":[{"id":"CVE-2026-26309","title":"Envoy has an off-by-one write in JsonEscaper::escapeString()","description":"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.","status":"published","published":"2026-03-10T19:04:21.384Z","modified":"2026-10-28T02:30:49.790966504Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L","level":"medium"}],"cwes":["CWE-193"],"updated_at":"2026-11-02T15:44:59.80315Z"},{"id":"GHSA-56cj-wgg3-x943","title":"Envoy affected by off-by-one write in JsonEscaper::escapeString()","description":"### 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, '\\\\');\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] = '\\\\'; 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] = '\\\\';)\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’t immediately crash, it can break the std::string\n  requirement that c_str()[size()] == '\\0', 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 '\\\"':\n      case '\\\\':\n      case '\\b':\n      case '\\f':\n      case '\\n':\n      case '\\r':\n      case '\\t':\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, '\\\\');\n    uint64_t position = 0;\n\n    for (unsigned char character : input) {\n      switch (character) {\n      case '\\\"':\n        result[position + 1] = '\\\"';\n        position += 2;\n        break;\n      case '\\\\':\n        position += 2;\n        break;\n      case '\\b':\n        result[position + 1] = 'b';\n        position += 2;\n        break;\n      case '\\f':\n        result[position + 1] = 'f';\n        position += 2;\n        break;\n      case '\\n':\n        result[position + 1] = 'n';\n        position += 2;\n        break;\n      case '\\r':\n        result[position + 1] = 'r';\n        position += 2;\n        break;\n      case '\\t':\n        result[position + 1] = 't';\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] = '\\\\';\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, 'A');\n    input.push_back('\\x01'); // 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  }```","status":"published","published":"2026-03-10T18:30:58Z","modified":"2026-03-13T20:26:07.599912Z","cvss":[{"version":"3.1","score":5.3,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L","level":"medium"}],"cwes":["CWE-193"],"updated_at":"2026-11-02T15:46:37.864461Z"},{"id":"BIT-envoy-2026-26309","title":"Envoy has an off-by-one write in JsonEscaper::escapeString()","description":"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.","status":"published","published":"2026-03-12T08:39:42.871Z","modified":"2026-09-08T08:47:30.606642961Z","updated_at":"2026-11-02T15:28:29.563843Z"}]}}
