Summary
Mint's HTTP/2 client accepts PUSH_PROMISE frames from any server it connects to and inserts every promised stream into a per-connection map without consulting max_concurrent_streams. A malicious or compromised HTTP/2 server can flood the client with PUSH_PROMISE frames and withhold the matching response HEADERS, pinning one map entry per frame indefinitely until the client process runs out of memory.
Details
'Elixir.Mint.HTTP2':handle_push_promise/3 in lib/mint/http2.ex dispatches every inbound PUSH_PROMISE frame to 'Elixir.Mint.HTTP2':decode_push_promise_headers_and_add_response/5, which inserts a :reserved_remote entry into conn.streams for the promised ID. The only validation applied is that the promised ID is even and not already present; client_settings.max_concurrent_streams is not consulted at promise time.
The concurrency cap is only checked when the response HEADERS for the promised stream arrive. A server that emits PUSH_PROMISE frames and never sends the matching HEADERS never trips that check, and the existing tally counts only streams in open states, not :reserved_remote entries.
HTTP/2 server push is accepted by default (client_settings.enable_push defaults to true), so no application opt-in is required. A single long-lived HTTP/2 connection to a hostile server lets it pin one conn.streams entry per PUSH_PROMISE frame, with no upper bound.
PoC
- Stand up a raw TCP HTTP/2 server that completes the handshake and ACKs the client's
SETTINGS.
- Wait for the client's request
HEADERS and capture its odd stream ID.
- Send a flood of
PUSH_PROMISE frames (flags = END_HEADERS) associated with the captured stream, each promising a fresh even stream ID and carrying a minimal HPACK-encoded header block.
- Never send the matching response
HEADERS for any of the promised IDs.
- The client's
conn.streams map grows by one entry per PUSH_PROMISE frame (~148 bytes/entry); memory grows linearly and the BEAM process eventually crashes with OOM.
Impact
Remote, unauthenticated denial-of-service against any process using Mint as an HTTP/2 client against an untrusted or attacker-influenced server. Server push is on by default, so no application code change can prevent it short of disabling push or upgrading. Affected populations include outbound HTTP/2 clients in web backends, webhook delivery systems, scrapers, federated and proxy components, and any service that follows redirects to third-party HTTP/2 origins.
Workarounds
Disable HTTP/2 server push on connections to untrusted servers by passing client_settings: [enable_push: false] to 'Elixir.Mint.HTTP':connect/4. Mint will then reject any inbound PUSH_PROMISE frame with a PROTOCOL_ERROR before the vulnerable code path is reached.
Resources
References
Summary
Mint's HTTP/2 client accepts
PUSH_PROMISEframes from any server it connects to and inserts every promised stream into a per-connection map without consultingmax_concurrent_streams. A malicious or compromised HTTP/2 server can flood the client withPUSH_PROMISEframes and withhold the matching responseHEADERS, pinning one map entry per frame indefinitely until the client process runs out of memory.Details
'Elixir.Mint.HTTP2':handle_push_promise/3inlib/mint/http2.exdispatches every inboundPUSH_PROMISEframe to'Elixir.Mint.HTTP2':decode_push_promise_headers_and_add_response/5, which inserts a:reserved_remoteentry intoconn.streamsfor the promised ID. The only validation applied is that the promised ID is even and not already present;client_settings.max_concurrent_streamsis not consulted at promise time.The concurrency cap is only checked when the response
HEADERSfor the promised stream arrive. A server that emitsPUSH_PROMISEframes and never sends the matchingHEADERSnever trips that check, and the existing tally counts only streams in open states, not:reserved_remoteentries.HTTP/2 server push is accepted by default (
client_settings.enable_pushdefaults totrue), so no application opt-in is required. A single long-lived HTTP/2 connection to a hostile server lets it pin oneconn.streamsentry perPUSH_PROMISEframe, with no upper bound.PoC
SETTINGS.HEADERSand capture its odd stream ID.PUSH_PROMISEframes (flags = END_HEADERS) associated with the captured stream, each promising a fresh even stream ID and carrying a minimal HPACK-encoded header block.HEADERSfor any of the promised IDs.conn.streamsmap grows by one entry perPUSH_PROMISEframe (~148 bytes/entry); memory grows linearly and the BEAM process eventually crashes with OOM.Impact
Remote, unauthenticated denial-of-service against any process using Mint as an HTTP/2 client against an untrusted or attacker-influenced server. Server push is on by default, so no application code change can prevent it short of disabling push or upgrading. Affected populations include outbound HTTP/2 clients in web backends, webhook delivery systems, scrapers, federated and proxy components, and any service that follows redirects to third-party HTTP/2 origins.
Workarounds
Disable HTTP/2 server push on connections to untrusted servers by passing
client_settings: [enable_push: false]to'Elixir.Mint.HTTP':connect/4. Mint will then reject any inboundPUSH_PROMISEframe with aPROTOCOL_ERRORbefore the vulnerable code path is reached.Resources
References