This is an automated email from the ASF dual-hosted git repository.
markt pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/main by this push:
new 9d379e26a7 Respond to invalid HTTP/2 headers appropriately
9d379e26a7 is described below
commit 9d379e26a71542dc1aee9580d895c4ed3a413435
Author: Mark Thomas <[email protected]>
AuthorDate: Tue Mar 10 14:04:20 2026 +0000
Respond to invalid HTTP/2 headers appropriately
Use as Stream reset or a 400 response rather than a connection reset
---
java/org/apache/coyote/Request.java | 4 +
java/org/apache/coyote/http2/Stream.java | 105 ++++++++++++++-------
java/org/apache/coyote/http2/StreamProcessor.java | 11 ++-
.../apache/coyote/http2/TestHttp2Section_8_1.java | 52 +++++++++-
webapps/docs/changelog.xml | 5 +
5 files changed, 142 insertions(+), 35 deletions(-)
diff --git a/java/org/apache/coyote/Request.java
b/java/org/apache/coyote/Request.java
index 5313780965..d50057a4e9 100644
--- a/java/org/apache/coyote/Request.java
+++ b/java/org/apache/coyote/Request.java
@@ -68,6 +68,10 @@ public final class Request {
*/
private static final AtomicLong requestIdGenerator = new AtomicLong(0);
+ // public static final int NOTE_ADAPTER = 1; // Defined in CoyoteAdapter
+ public static final int NOTE_BAD_REQUEST = 2;
+
+
// ----------------------------------------------------------- Constructors
public Request() {
diff --git a/java/org/apache/coyote/http2/Stream.java
b/java/org/apache/coyote/http2/Stream.java
index 494ca38b40..e7c0d23531 100644
--- a/java/org/apache/coyote/http2/Stream.java
+++ b/java/org/apache/coyote/http2/Stream.java
@@ -324,17 +324,28 @@ class Stream extends AbstractNonZeroStream implements
HeaderEmitter {
// Header names must be lowercase
if (!name.toLowerCase(Locale.US).equals(name)) {
- throw new HpackException(sm.getString("stream.header.case",
getConnectionId(), getIdAsString(), name));
+ headerException =
+ new StreamException(sm.getString("stream.headercase",
getConnectionId(), getIdAsString(), name),
+ Http2Error.PROTOCOL_ERROR, getIdAsInt());
+ // No need for further processing. The stream will be reset.
+ return;
}
if (HTTP_CONNECTION_SPECIFIC_HEADERS.contains(name)) {
- throw new HpackException(
- sm.getString("stream.header.connection",
getConnectionId(), getIdAsString(), name));
+ headerException = new StreamException(
+ sm.getString("stream.header.connection",
getConnectionId(), getIdAsString(), name),
+ Http2Error.PROTOCOL_ERROR, getIdAsInt());
+ // No need for further processing. The stream will be reset.
+ return;
}
if ("te".equals(name)) {
if (!"trailers".equals(value)) {
- throw new HpackException(sm.getString("stream.header.te",
getConnectionId(), getIdAsString(), value));
+ headerException =
+ new StreamException(sm.getString("stream.header.te",
getConnectionId(), getIdAsString(), name),
+ Http2Error.PROTOCOL_ERROR, getIdAsInt());
+ // No need for further processing. The stream will be reset.
+ return;
}
}
@@ -345,7 +356,11 @@ class Stream extends AbstractNonZeroStream implements
HeaderEmitter {
}
if (name.isEmpty()) {
- throw new HpackException(sm.getString("stream.header.empty",
getConnectionId(), getIdAsString()));
+ headerException =
+ new StreamException(sm.getString("stream.header.empty",
getConnectionId(), getIdAsString(), name),
+ Http2Error.PROTOCOL_ERROR, getIdAsInt());
+ // No need for further processing. The stream will be reset.
+ return;
}
boolean pseudoHeader = name.charAt(0) == ':';
@@ -370,8 +385,11 @@ class Stream extends AbstractNonZeroStream implements
HeaderEmitter {
configureVoidOutputFilter();
}
} else {
- throw new HpackException(
- sm.getString("stream.header.duplicate",
getConnectionId(), getIdAsString(), ":method"));
+ headerException = new StreamException(
+ sm.getString("stream.header.duplicate",
getConnectionId(), getIdAsString(), ":method"),
+ Http2Error.PROTOCOL_ERROR, getIdAsInt());
+ // No need for further processing. The stream will be
reset.
+ return;
}
break;
}
@@ -379,18 +397,28 @@ class Stream extends AbstractNonZeroStream implements
HeaderEmitter {
if (coyoteRequest.scheme().isNull()) {
coyoteRequest.scheme().setString(value);
} else {
- throw new HpackException(
- sm.getString("stream.header.duplicate",
getConnectionId(), getIdAsString(), ":scheme"));
+ headerException = new StreamException(
+ sm.getString("stream.header.duplicate",
getConnectionId(), getIdAsString(), ":scheme"),
+ Http2Error.PROTOCOL_ERROR, getIdAsInt());
+ // No need for further processing. The stream will be
reset.
+ return;
}
break;
}
case ":path": {
if (!coyoteRequest.requestURI().isNull()) {
- throw new HpackException(
- sm.getString("stream.header.duplicate",
getConnectionId(), getIdAsString(), ":path"));
+ headerException = new StreamException(
+ sm.getString("stream.header.duplicate",
getConnectionId(), getIdAsString(), ":path"),
+ Http2Error.PROTOCOL_ERROR, getIdAsInt());
+ // No need for further processing. The stream will be
reset.
+ return;
}
if (value.isEmpty()) {
- throw new
HpackException(sm.getString("stream.header.noPath", getConnectionId(),
getIdAsString()));
+ headerException = new StreamException(
+ sm.getString("stream.header.noPath",
getConnectionId(), getIdAsString()),
+ Http2Error.PROTOCOL_ERROR, getIdAsInt());
+ // No need for further processing. The stream will be
reset.
+ return;
}
int queryStart = value.indexOf('?');
String uri;
@@ -411,10 +439,13 @@ class Stream extends AbstractNonZeroStream implements
HeaderEmitter {
}
case ":authority": {
if (coyoteRequest.serverName().isNull()) {
- parseAuthority(value, false);
+ parseAuthority(value);
} else {
- throw new HpackException(
- sm.getString("stream.header.duplicate",
getConnectionId(), getIdAsString(), ":authority"));
+ headerException = new StreamException(
+ sm.getString("stream.header.duplicate",
getConnectionId(), getIdAsString(), ":authority"),
+ Http2Error.PROTOCOL_ERROR, getIdAsInt());
+ // No need for further processing. The stream will be
reset.
+ return;
}
break;
}
@@ -433,15 +464,18 @@ class Stream extends AbstractNonZeroStream implements
HeaderEmitter {
if (coyoteRequest.serverName().isNull()) {
// No :authority header. This is first host header. Use it.
hostHeaderSeen = true;
- parseAuthority(value, true);
+ parseAuthority(value);
} else if (!hostHeaderSeen) {
// First host header - must be consistent with :authority
hostHeaderSeen = true;
compareAuthority(value);
} else {
// Multiple hosts headers - illegal
- throw new HpackException(
- sm.getString("stream.header.duplicate",
getConnectionId(), getIdAsString(), "host"));
+ headerException = new StreamException(
+ sm.getString("stream.header.duplicate",
getConnectionId(), getIdAsString(), "host"),
+ Http2Error.PROTOCOL_ERROR, getIdAsInt());
+ // No need for further processing. The stream will be
reset.
+ return;
}
break;
}
@@ -491,7 +525,7 @@ class Stream extends AbstractNonZeroStream implements
HeaderEmitter {
streamOutputBuffer.closed = true;
}
- private void parseAuthority(String value, boolean host) throws
HpackException {
+ private void parseAuthority(String value) {
int i;
try {
i = Host.parse(value);
@@ -502,33 +536,40 @@ class Stream extends AbstractNonZeroStream implements
HeaderEmitter {
coyoteRequest.serverName().setString(value);
}
} catch (IllegalArgumentException iae) {
- // Host value invalid
- throw new HpackException(sm.getString("stream.header.invalid",
getConnectionId(), getIdAsString(),
- host ? "host" : ":authority", value));
+ // Bad :authority / host header -> 400 response
+ coyoteRequest.setNote(Request.NOTE_BAD_REQUEST, Boolean.TRUE);
}
// Match host name with SNI if required
- if
(!handler.getProtocol().getHttp11Protocol().checkSni(handler.getSniHostName(),
coyoteRequest.serverName().getString())) {
- throw new HpackException(sm.getString("stream.host.sni",
getConnectionId(), getIdAsString(), value,
- handler.getSniHostName()));
+ if
(!handler.getProtocol().getHttp11Protocol().checkSni(handler.getSniHostName(),
+ coyoteRequest.serverName().getString())) {
+ headerException = new StreamException(
+ sm.getString("stream.host.sni", getConnectionId(),
getIdAsString(), value),
+ Http2Error.PROTOCOL_ERROR, getIdAsInt());
+ // No need for further processing. The stream will be reset.
+ return;
}
}
- private void compareAuthority(String value) throws HpackException {
+ private void compareAuthority(String value) {
int i;
try {
i = Host.parse(value);
- if (i == -1 &&
(!value.equals(coyoteRequest.serverName().getString()) ||
coyoteRequest.getServerPort() != -1) ||
+ if (i == -1 &&
+ (!value.equals(coyoteRequest.serverName().getString()) ||
coyoteRequest.getServerPort() != -1) ||
i > -1 && ((!value.substring(0,
i).equals(coyoteRequest.serverName().getString()) ||
Integer.parseInt(value.substring(i + 1)) !=
coyoteRequest.getServerPort()))) {
// Host value inconsistent
- throw new
HpackException(sm.getString("stream.host.inconsistent", getConnectionId(),
getIdAsString(), value,
- coyoteRequest.serverName().getString(),
Integer.toString(coyoteRequest.getServerPort())));
+ headerException = new StreamException(
+ sm.getString("stream.host.inconsistent",
getConnectionId(), getIdAsString(), value,
+ coyoteRequest.serverName().getString(),
Integer.toString(coyoteRequest.getServerPort())),
+ Http2Error.PROTOCOL_ERROR, getIdAsInt());
+ // No need for further processing. The stream will be reset.
+ return;
}
} catch (IllegalArgumentException iae) {
- // Host value invalid
- throw new HpackException(
- sm.getString("stream.header.invalid", getConnectionId(),
getIdAsString(), "host", value));
+ // Bad :authority / host header -> 400 response
+ coyoteRequest.setNote(Request.NOTE_BAD_REQUEST, Boolean.TRUE);
}
}
diff --git a/java/org/apache/coyote/http2/StreamProcessor.java
b/java/org/apache/coyote/http2/StreamProcessor.java
index 942ed2014c..0b9b311031 100644
--- a/java/org/apache/coyote/http2/StreamProcessor.java
+++ b/java/org/apache/coyote/http2/StreamProcessor.java
@@ -502,7 +502,14 @@ class StreamProcessor extends AbstractProcessor implements
NonPipeliningProcesso
* The checks performed below are based on the checks in Http11InputBuffer.
*/
private boolean validateRequest() {
- HttpParser httpParser =
handler.getProtocol().getHttp11Protocol().getHttpParser();
+ // Check for issues during header processing. Include:
+ // - invalid (incorrectly formatted) :authority header
+ // - invalid (incorrectly formatted) host header
+ if (request.getNote(Request.NOTE_BAD_REQUEST) != null) {
+ // Notes not reset when request is recycled
+ request.setNote(Request.NOTE_BAD_REQUEST, null);
+ return false;
+ }
// Method name must be a token
if (!HttpParser.isToken(request.getMethod())) {
@@ -515,6 +522,8 @@ class StreamProcessor extends AbstractProcessor implements
NonPipeliningProcesso
return false;
}
+ HttpParser httpParser =
handler.getProtocol().getHttp11Protocol().getHttpParser();
+
// Invalid character in request target
// (other checks such as valid %nn happen later)
ByteChunk bc = request.requestURI().getByteChunk();
diff --git a/test/org/apache/coyote/http2/TestHttp2Section_8_1.java
b/test/org/apache/coyote/http2/TestHttp2Section_8_1.java
index 54f2e50431..fce4bccccc 100644
--- a/test/org/apache/coyote/http2/TestHttp2Section_8_1.java
+++ b/test/org/apache/coyote/http2/TestHttp2Section_8_1.java
@@ -280,6 +280,54 @@ public class TestHttp2Section_8_1 extends Http2TestBase {
}
+ @Test
+ public void testHostHeaderInvalid() throws Exception {
+ http2Connect();
+
+ List<Header> headers = new ArrayList<>(4);
+ headers.add(new Header(":method", Method.GET));
+ headers.add(new Header(":scheme", "http"));
+ headers.add(new Header(":path", "/simple"));
+ headers.add(new Header("host", "local!host:" + getPort()));
+
+ byte[] headersFrameHeader = new byte[9];
+ ByteBuffer headersPayload = ByteBuffer.allocate(128);
+
+ buildGetRequest(headersFrameHeader, headersPayload, null, headers, 3);
+
+ writeFrame(headersFrameHeader, headersPayload);
+
+ parser.readFrame();
+
+ String trace = output.getTrace();
+ Assert.assertTrue(trace, trace.contains("3-Header-[:status]-[400]"));
+ }
+
+
+ @Test
+ public void testAuthorityHeaderInvalid() throws Exception {
+ http2Connect();
+
+ List<Header> headers = new ArrayList<>(4);
+ headers.add(new Header(":method", Method.GET));
+ headers.add(new Header(":scheme", "http"));
+ headers.add(new Header(":path", "/simple"));
+ headers.add(new Header(":authority", "local!host:" + getPort()));
+
+ byte[] headersFrameHeader = new byte[9];
+ ByteBuffer headersPayload = ByteBuffer.allocate(128);
+
+ buildGetRequest(headersFrameHeader, headersPayload, null, headers, 3);
+
+ writeFrame(headersFrameHeader, headersPayload);
+
+ parser.readFrame();
+
+ String trace = output.getTrace();
+ Assert.assertTrue(trace, trace.contains("3-Header-[:status]-[400]"));
+ }
+
+
@Test
public void testHostHeaderDuplicate() throws Exception {
http2Connect();
@@ -301,7 +349,7 @@ public class TestHttp2Section_8_1 extends Http2TestBase {
parser.readFrame();
String trace = output.getTrace();
- Assert.assertTrue(trace, trace.contains("0-Goaway-[1]-[9]"));
+ Assert.assertTrue(trace, trace.contains("3-RST-[1]"));
}
@@ -413,7 +461,7 @@ public class TestHttp2Section_8_1 extends Http2TestBase {
parser.readFrame();
String trace = output.getTrace();
- Assert.assertTrue(trace, trace.contains("0-Goaway-[1]-[9]"));
+ Assert.assertTrue(trace, trace.contains("3-RST-[1]"));
}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 279d2d82f5..eae2c7bea2 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -288,6 +288,11 @@
Replace the external OpenSSL based OCSP responder used during unit
tests
with a Bouncy Castle based, in-process Java OCSP responder. (markt)
</scode>
+ <fix>
+ Relax HTTP/2 header validation and respond to invalid requests with a
+ stream reset or a 400 response as appropriate rather then with a
+ connection reset. (markt)
+ </fix>
</changelog>
</subsection>
<subsection name="Jasper">
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]