From 98932d097654f9a3697beb270601e9f53a45a8d2 Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Mon, 23 Feb 2026 09:49:15 +0100 Subject: [PATCH] HTTP/2: require peer SETTINGS as first frame Enforce RFC 9113 connection preface rules by rejecting non-SETTINGS frames received before the peer SETTINGS once the connection handshake is active. --- .../impl/nio/AbstractH2StreamMultiplexer.java | 10 ++++- .../nio/TestAbstractH2StreamMultiplexer.java | 42 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/AbstractH2StreamMultiplexer.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/AbstractH2StreamMultiplexer.java index fb9a07795..9bc5b6b15 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/AbstractH2StreamMultiplexer.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/AbstractH2StreamMultiplexer.java @@ -772,6 +772,10 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio final FrameType frameType = FrameType.valueOf(frame.getType()); final int streamId = frame.getStreamId() & 0x7fffffff; + // once the connection is active, the first frame from a peer after the preface MUST be SETTINGS. + if (connState == ConnectionHandshake.ACTIVE && remoteSettingState == SettingsHandshake.READY && frameType != FrameType.SETTINGS) { + throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "SETTINGS frame expected as first peer frame"); + } if (continuation != null && frameType != FrameType.CONTINUATION) { throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "CONTINUATION frame expected"); } @@ -955,11 +959,15 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Illegal stream id"); } if (frame.isFlagSet(FrameFlag.ACK)) { - // RFC 9113, Section 6.5: SETTINGS with ACK set MUST have an empty payload. + // SETTINGS with ACK set MUST have an empty payload. final ByteBuffer payload = frame.getPayload(); if (payload != null && payload.hasRemaining()) { throw new H2ConnectionException(H2Error.FRAME_SIZE_ERROR, "Invalid SETTINGS ACK payload"); } + // The first peer SETTINGS cannot be ACK. + if (connState == ConnectionHandshake.ACTIVE && remoteSettingState == SettingsHandshake.READY) { + throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Illegal SETTINGS ACK"); + } if (localSettingState == SettingsHandshake.TRANSMITTED) { localSettingState = SettingsHandshake.ACKED; ioSession.setEvent(SelectionKey.OP_WRITE); diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestAbstractH2StreamMultiplexer.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestAbstractH2StreamMultiplexer.java index 5bb59e541..ea16173a3 100644 --- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestAbstractH2StreamMultiplexer.java +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestAbstractH2StreamMultiplexer.java @@ -1889,4 +1889,46 @@ void testHeadersWithPriorityFlagAndShortPayloadRejected() throws Exception { } + @Test + void testFirstPeerFrameMustBeSettings() throws Exception { + final AbstractH2StreamMultiplexer mux = new H2StreamMultiplexerImpl( + protocolIOSession, + FRAME_FACTORY, + StreamIdGenerator.ODD, + httpProcessor, + CharCodingConfig.DEFAULT, + H2Config.custom().build(), + h2StreamListener, + () -> streamHandler); + + mux.onConnect(); + + final RawFrame ping = new RawFrame(FrameType.PING.getValue(), 0, 0, ByteBuffer.wrap(new byte[8])); + final H2ConnectionException ex = Assertions.assertThrows( + H2ConnectionException.class, + () -> mux.onInput(ByteBuffer.wrap(encodeFrame(ping)))); + Assertions.assertEquals(H2Error.PROTOCOL_ERROR, H2Error.getByCode(ex.getCode())); + } + + @Test + void testFirstPeerSettingsAckRejected() throws Exception { + final AbstractH2StreamMultiplexer mux = new H2StreamMultiplexerImpl( + protocolIOSession, + FRAME_FACTORY, + StreamIdGenerator.ODD, + httpProcessor, + CharCodingConfig.DEFAULT, + H2Config.custom().build(), + h2StreamListener, + () -> streamHandler); + + mux.onConnect(); + + final RawFrame settingsAck = new RawFrame(FrameType.SETTINGS.getValue(), FrameFlag.ACK.getValue(), 0, null); + final H2ConnectionException ex = Assertions.assertThrows( + H2ConnectionException.class, + () -> mux.onInput(ByteBuffer.wrap(encodeFrame(settingsAck)))); + Assertions.assertEquals(H2Error.PROTOCOL_ERROR, H2Error.getByCode(ex.getCode())); + } + } \ No newline at end of file