VYPR
Medium severityNVD Advisory· Published Jun 8, 2026

CVE-2026-43972

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

2
  • Ninenines/Gunreferences2 versions
    (expand)+ 1 more
    • (no CPE)
    • (no CPE)range: from 2.0.0 before 2.4.0

Patches

1
567863ff5380

Restrict push promises to original request authority

https://github.com/ninenines/gunLoïc HoguinJun 5, 2026via body-scan
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

3

News mentions

0

No linked articles in our index yet.