CVE-2026-43972
Description
Origin Validation Error vulnerability in ninenines gun (gun_http2 module) allows cross-origin cookie injection via unvalidated HTTP/2 PUSH_PROMISE authority.
In gun_http2:push_promise_frame/7, the :authority pseudo-header from an incoming PUSH_PROMISE frame is stored verbatim into the promised stream record without checking that it matches the connection's origin. When gun_http2:headers_frame/9 later processes the response headers for the promised stream, it calls gun_cookies:set_cookie_header/7 with the unvalidated server-supplied authority before any status branching and before user code can act. This violates RFC 7540 §10.6 / RFC 9113 §8.4, which require receivers to treat as a protocol error any push for a resource the server is not authoritative for.
A malicious or compromised HTTP/2 server can plant cookies scoped to arbitrary third-party domains into the client's shared cookie store. This enables session fixation attacks against those domains and, if the planted cookie overrides a legitimate session token, may result in account takeover. No user interaction beyond making a normal HTTP/2 request to the attacker-controlled server is required.
This issue affects gun: from 2.0.0 before 2.4.0.
Affected products
2Patches
1567863ff5380Restrict push promises to original request authority
2 files changed · +72 −14
src/gun_http2.erl+36 −14 modified@@ -781,38 +781,60 @@ rst_stream_frame(State0, StreamID, Reason, EvHandler, EvHandlerState0) -> push_promise_frame(State=#http2_state{socket=Socket, transport=Transport, status=Status, http2_machine=HTTP2Machine0}, StreamID, PromisedStreamID, Headers, #{ - method := Method, scheme := Scheme, - authority := Authority, path := Path}, + method := PromisedMethod, scheme := PromisedScheme, + authority := PromisedAuthority, path := PromisedPath}, EvHandler, EvHandlerState0) -> - #stream{ref=StreamRef, reply_to=ReplyTo, flow=InitialFlow} = get_stream_by_id(State, StreamID), + #stream{ + ref=StreamRef, + authority=Authority, + reply_to=ReplyTo, + flow=InitialFlow + } = get_stream_by_id(State, StreamID), + Scheme = scheme(State), + %% We cancel the push_promise immediately when we are shutting + %% down or when the scheme/authority doesn't match the request's. + %% @todo We may wish to extend valid authorities to those that + %% are covered by the server's TLS certificate. + OKOrError = case Status of + connected -> + case {Scheme, iolist_to_binary(Authority)} of + {PromisedScheme, PromisedAuthority} -> ok; + _ -> protocol_error + end; + _ -> + cancel + end, PromisedStreamRef = make_ref(), RealPromisedStreamRef = stream_ref(State, PromisedStreamRef), - URI = iolist_to_binary([Scheme, <<"://">>, Authority, Path]), + URI = iolist_to_binary([PromisedScheme, <<"://">>, PromisedAuthority, PromisedPath]), PushPromiseEvent0 = #{ stream_ref => stream_ref(State, StreamRef), reply_to => ReplyTo, - method => Method, + method => PromisedMethod, uri => URI, headers => Headers }, - PushPromiseEvent = case Status of - connected -> + PushPromiseEvent = case OKOrError of + ok -> gun:reply(ReplyTo, {gun_push, self(), stream_ref(State, StreamRef), - RealPromisedStreamRef, Method, URI, Headers}), + RealPromisedStreamRef, PromisedMethod, URI, Headers}), PushPromiseEvent0#{promised_stream_ref => RealPromisedStreamRef}; _ -> PushPromiseEvent0 end, EvHandlerState = EvHandler:push_promise_end(PushPromiseEvent, EvHandlerState0), - case Status of - connected -> - NewStream = #stream{id=PromisedStreamID, ref=PromisedStreamRef, - reply_to=ReplyTo, flow=InitialFlow, authority=Authority, path=Path}, + case OKOrError of + ok -> + NewStream = #stream{ + id=PromisedStreamID, ref=PromisedStreamRef, + reply_to=ReplyTo, flow=InitialFlow, + authority=PromisedAuthority, path=PromisedPath + }, {{state, create_stream(State, NewStream)}, EvHandlerState}; - %% We cancel the push_promise immediately when we are shutting down. + %% Invalid push_promise gets canceled immediately. _ -> {ok, HTTP2Machine} = cow_http2_machine:reset_stream(PromisedStreamID, HTTP2Machine0), - case Transport:send(Socket, cow_http2:rst_stream(PromisedStreamID, cancel)) of + case Transport:send(Socket, cow_http2:rst_stream(PromisedStreamID, OKOrError)) of ok -> {{state, State#http2_state{http2_machine=HTTP2Machine}}, EvHandlerState}; Error={error, _} ->
test/rfc7540_SUITE.erl+36 −0 modified@@ -555,6 +555,42 @@ do_ping_ack_loop_fun() -> Loop(Parent, ListenSocket, Socket, Transport) end. +push_promise_invalid_authority(_) -> + doc("An authority in PUSH_PROMISE for which the server is not " + "authoritative must be rejected with a PROTOCOL_ERROR " + "stream error. (RFC7540 8.2, RFC9113 8.4)"), + {ok, OriginPid, Port} = init_origin(tcp, http2, fun(Parent, _, Socket, Transport) -> + %% Receive a HEADERS frame. + {ok, <<SkipLen:24, 1:8, _:8, 1:32>>} = Transport:recv(Socket, 9, 1000), + %% Skip the header. + {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), + %% Send a PUSH_PROMISE frame. + {HeadersBlock, _} = cow_hpack:encode([ + {<<":method">>, <<"GET">>}, + {<<":scheme">>, <<"http">>}, + {<<":authority">>, <<"gun.test">>}, + {<<":path">>, <<"/">>} + ]), + ok = Transport:send(Socket, [ + cow_http2:push_promise(1, 2, HeadersBlock) + ]), + %% Receive a PROTOCOL_ERROR RST_STREAM for pushed stream. + {ok, << 4:24, 3:8, 2:40, 1:32 >>} = gen_tcp:recv(Socket, 13, 1000), + Parent ! done, + timer:sleep(5000) + end), + {ok, ConnPid} = gun:open("localhost", Port, #{ + protocols => [http2] + }), + {ok, http2} = gun:await_up(ConnPid), + handshake_completed = receive_from(OriginPid), + %% Step 1. + StreamRef = gun:get(ConnPid, "/"), + %% Confirm we never get the gun_push message. + {error, timeout} = gun:await(ConnPid, StreamRef, 1000), + receive done -> ok end, + gun:close(ConnPid). + connect_http_via_h2c(_) -> doc("CONNECT can be used to establish a TCP connection " "to an HTTP/1.1 server via a TCP HTTP/2 proxy. (RFC7540 8.3)"),
Vulnerability mechanics
Root cause
"The HTTP/2 module fails to validate the authority of PUSH_PROMISE frames against the connection's origin."
Attack vector
An attacker controls an HTTP/2 server. The attacker sends a PUSH_PROMISE frame with an arbitrary `:authority` pseudo-header. The server's HTTP/2 client then processes this frame, potentially storing cookies scoped to the attacker-controlled domain. This can lead to session fixation or account takeover if the planted cookie overrides legitimate session tokens. No user interaction beyond making a normal HTTP/2 request to the attacker-controlled server is required [ref_id=1].
Affected code
The vulnerability resides in the `gun_http2:push_promise_frame/7` function within the `src/gun_http2.erl` file. This function is responsible for handling incoming PUSH_PROMISE frames in the HTTP/2 protocol implementation [ref_id=1].
What the fix does
The patch modifies the `push_promise_frame/7` function in `src/gun_http2.erl` [patch_id=5216825]. It now checks if the `Scheme` and `Authority` from the PUSH_PROMISE frame match the connection's `Scheme` and `Authority`. If they do not match, or if the connection is not in a `connected` state, the push promise is canceled with a `protocol_error` or `cancel` error, respectively, preventing the injection of unvalidated authorities and subsequent cookie planting [ref_id=1].
Preconditions
- networkThe client must establish an HTTP/2 connection with a malicious or compromised server.
Generated on Jun 8, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
3News mentions
0No linked articles in our index yet.