{"id":"EEF-CVE-2026-105659","summary":"HTTP/2 blocked-send deadlines reset on every WINDOW_UPDATE, letting a trickle pin stream processes indefinitely in Bandit","details":"## Summary\n\nUncontrolled Resource Consumption vulnerability in mtrudel bandit allows an unauthenticated remote attacker to pin HTTP/2 stream processes indefinitely via a trickle of small `WINDOW_UPDATE` frames. A stream whose response outruns the connection or stream send window blocks until the peer grants more window, and each block carries a 15 second deadline. Both deadlines restart whenever the peer makes partial progress. `Bandit.HTTP2.Connection.finish_data/5` re-queues a partially advanced send in `pending_sends` with a fresh `expires_at`, so every `WINDOW_UPDATE` on stream 0, and every `SETTINGS` frame, resets the clock. `Bandit.HTTP2.Stream.send_data/3` recurses on each stream `WINDOW_UPDATE` and re-arms a fresh `after read_timeout`. A peer that grants a few bytes every few seconds keeps the blocked stream process, its Plug state and any resource it holds mid-response alive for as long as it keeps dripping, and repeats this across streams and connections to exhaust server processes and memory.\n\nThe two paths are independent. The stream-level block is reached on its own by widening the connection window with a large `WINDOW_UPDATE` on stream 0 and shrinking the per-stream window with a small `SETTINGS_INITIAL_WINDOW_SIZE`, so closing only the connection-level path leaves it exploitable.\n\nThis issue affects bandit: from 1.4.0 onward.\n\n## Details\n\n**1. Connection-level block** (`lib/bandit/http2/connection.ex`)\n\nA send that outruns the connection send window is queued in `pending_sends` with an `expires_at` deadline computed in `finish_data/5` as now plus `@pending_send_timeout` (15 seconds). `expire_pending_sends/1` drops any queued send whose deadline has passed whenever data arrives on the connection. `do_pending_sends/2` runs on every `WINDOW_UPDATE` on stream 0 and on every `SETTINGS` frame (including an empty one, through `handle_frame/3`). When the granted window is smaller than the queued remainder, the leftover is re-queued through `finish_data/5`, which recomputes `expires_at` from the current time instead of preserving the original deadline. Each partial advance therefore restarts the timer, and an empty `SETTINGS` frame restarts it without granting any window at all.\n\n**2. Stream-level block** (`lib/bandit/http2/stream.ex`)\n\n`send_data/3` runs in the stream process. When the stream send window is exhausted it enters a `receive` that waits for `{:bandit, {:send_window_update, delta}}` with `after stream.read_timeout` (the struct default of 15 seconds, never overridden in `init/5`). On each stream `WINDOW_UPDATE` it applies the delta and recurses into `send_data/3` with the remainder, which re-enters the same `receive` with a fresh `after` timeout. Each partial advance restarts the 15 second wait.\n\nFor the stream-level block to be the one stuck, the stream window must be exhausted while the connection window has room. The peer controls both: a large `WINDOW_UPDATE` on stream 0 opens the connection window, and a small `SETTINGS_INITIAL_WINDOW_SIZE` shrinks every stream window. Closing only the connection-level path therefore leaves the stream-level path fully exploitable.\n\n**3. Effect**\n\nThe trickle also keeps the transport-level read timeout from firing, since the connection continuously receives valid frames. The blocked stream process, its Plug state, and anything it holds mid-response stay retained for as long as the peer keeps dripping, instead of being released after 15 seconds.\n\n## Proof of concept\n\nConnection-level path:\n\n1. Open a stream whose response exceeds the connection send window (65,535 bytes by default).\n2. The server sends the window's worth and queues the remainder in `pending_sends`.\n3. Send `WINDOW_UPDATE` for stream 0 with an increment of 1 every few seconds, or an empty `SETTINGS` frame. Each one re-queues the remainder with a fresh deadline, so `expire_pending_sends/1` never expires it.\n\nStream-level path:\n\n1. Send a large `WINDOW_UPDATE` for stream 0 so the connection window never blocks.\n2. Advertise a small `SETTINGS_INITIAL_WINDOW_SIZE` so each stream window is tiny.\n3. Request a response larger than the stream window. The server sends the window's worth and blocks in the recursive `receive` in `send_data/3`.\n4. Send `WINDOW_UPDATE` for the stream with an increment of 1 at intervals shorter than 15 seconds. Each one re-arms a fresh `read_timeout`.\n5. Repeat across streams, up to `max_concurrent_streams` per connection, and across connections.\n\n## Impact\n\nA remote, unauthenticated peer requests a response larger than a send window, then drips tiny `WINDOW_UPDATE` frames every few seconds. Each blocked stream process, with its Plug state and any upstream connection or file handle it holds, stays alive for as long as the trickle continues. Repeated across many streams and connections this exhausts server processes and memory.\n\n## Workarounds\n\nDisable HTTP/2 with `http_2_options: [enabled: false]`, which removes both vulnerable paths. Alternatively, terminate HTTP/2 at a reverse proxy that enforces its own send timeouts and speaks HTTP/1 to Bandit.","aliases":["CVE-2026-105659","GHSA-69m6-wcv4-rwcf"],"modified":"2026-10-14T17:11:10.789733592Z","published":"2026-10-14T16:54:46.484Z","database_specific":{"capec_ids":["CAPEC-227"],"cpe_ids":["cpe:2.3:a:mtrudel:bandit:*:*:*:*:*:*:*:*"],"cwe_ids":["CWE-400"]},"references":[{"type":"ADVISORY","url":"https://github.com/mtrudel/bandit/security/advisories/GHSA-69m6-wcv4-rwcf"},{"type":"WEB","url":"https://cna.erlef.org/cves/CVE-2026-105659.html"},{"type":"WEB","url":"https://github.com/mtrudel/bandit/commit/fff06efe3be962b484bd4d3eb339372fda5a231f"},{"type":"FIX","url":"https://github.com/mtrudel/bandit/commit/d88b21953c65061f21136ea76a89a4f95a2ac7a1"},{"type":"PACKAGE","url":"https://hex.pm/packages/bandit"}],"affected":[{"package":{"name":"bandit","ecosystem":"Hex","purl":"pkg:hex/bandit"},"ranges":[{"type":"SEMVER","events":[{"introduced":"1.4.0"}]}],"versions":["1.10.0","1.10.1","1.10.2","1.10.3","1.10.4","1.11.0","1.11.1","1.12.0","1.12.1","1.12.2","1.12.3","1.12.4","1.12.5","1.4.0","1.4.1","1.4.2","1.5.0","1.5.1","1.5.2","1.5.3","1.5.4","1.5.5","1.5.6","1.5.7","1.6.0","1.6.1","1.6.10","1.6.11","1.6.2","1.6.3","1.6.4","1.6.5","1.6.6","1.6.7","1.6.8","1.6.9","1.7.0","1.8.0","1.9.0"],"database_specific":{"source":"https://cna.erlef.org/osv/EEF-CVE-2026-105659.json"}},{"ranges":[{"type":"GIT","repo":"https://github.com/mtrudel/bandit","events":[{"introduced":"fff06efe3be962b484bd4d3eb339372fda5a231f"},{"fixed":"d88b21953c65061f21136ea76a89a4f95a2ac7a1"}]}],"versions":["1.12.5","1.12.4","1.12.3","1.12.1","1.12.0","1.11.1","1.11.0","1.10.4","1.10.3","1.10.2","1.10.1","1.10.0","1.9.0","1.8.0","1.7.0","1.6.11","1.6.10","1.6.9","1.6.8","1.6.7","1.6.6","1.6.5","1.6.4","1.6.3","1.6.2","1.6.1","1.6.0","1.5.7","1.5.6","1.5.5","1.5.4","1.5.3","1.5.2","1.5.1","1.5.0","1.4.2","1.4.1","1.4.0"],"database_specific":{"source":"https://cna.erlef.org/osv/EEF-CVE-2026-105659.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N"}],"credits":[{"name":"Thomas Cioppettini","type":"FINDER"},{"name":"Thomas Cioppettini","type":"REPORTER"},{"name":"Jonatan Männchen / EEF","type":"COORDINATOR"},{"name":"Mat Trudel","type":"REMEDIATION_DEVELOPER"}]}