This is an automated email from the ASF dual-hosted git repository.
markt-asf pushed a commit to branch 10.1.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/10.1.x by this push:
new 259e938d3d Revert & re-implement "Add HTTP/2 header filtering and
associated tests"
259e938d3d is described below
commit 259e938d3dedf07f3b24189fd5032adb95b01f2a
Author: Mark Thomas <[email protected]>
AuthorDate: Mon Sep 7 15:22:24 2026 +0100
Revert & re-implement "Add HTTP/2 header filtering and associated tests"
While moving the validation into the HPACK decoder was marginally more
efficient, it made correct handling of any errors much more difficult as
the HACK decoder does not have access to the current validation state.
That is managed by Stream where the remaining header validation takes
place. All header validation now takes place in Stream which is much
cleaner. Some deprecated code will be "un-deprecated" as a result.
---
java/org/apache/coyote/http2/HPackHuffman.java | 77 ++--------
java/org/apache/coyote/http2/HpackDecoder.java | 35 ++---
java/org/apache/coyote/http2/Http2Parser.java | 2 -
.../apache/coyote/http2/LocalStrings.properties | 10 +-
java/org/apache/coyote/http2/Stream.java | 67 +++++++--
test/org/apache/coyote/http2/TestHPackHuffman.java | 2 +-
.../apache/coyote/http2/TestHttp2Section_4_3.java | 162 +++++++++++++++++++++
webapps/docs/changelog.xml | 5 +
8 files changed, 255 insertions(+), 105 deletions(-)
diff --git a/java/org/apache/coyote/http2/HPackHuffman.java
b/java/org/apache/coyote/http2/HPackHuffman.java
index 784f9f175c..625b457317 100644
--- a/java/org/apache/coyote/http2/HPackHuffman.java
+++ b/java/org/apache/coyote/http2/HPackHuffman.java
@@ -22,7 +22,6 @@ import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
-import org.apache.tomcat.util.http.parser.HttpParser;
import org.apache.tomcat.util.res.StringManager;
/**
@@ -376,44 +375,44 @@ public class HPackHuffman {
DECODING_TABLE = codingTree;
}
+
/**
* Decodes a huffman encoded string into the target StringBuilder. There
must be enough space left in the buffer for
* this method to succeed.
*
- * @param data The byte buffer
- * @param length The length of data from the buffer to decode
- * @param target The target for the decompressed data
+ * @param data The byte buffer
+ * @param length The length of data from the buffer to decode
+ * @param target The target for the decompressed data
+ * @param isFieldName {@code true} if a field name is being decoded (names
have a more restrictive set of allowed
+ * characters than field values)
*
* @throws HpackException If the Huffman encoded value in HPACK headers
did not end with EOS padding
*
* @deprecated Will be removed in Tomcat 12. Use {@link
#decode(ByteBuffer, int, StringBuilder, boolean)}
*/
@Deprecated
- public static void decode(ByteBuffer data, int length, StringBuilder
target) throws HpackException {
- decode(data, length, target, false);
+ public static void decode(ByteBuffer data, int length, StringBuilder
target, boolean isFieldName)
+ throws HpackException {
+ decode(data, length, target);
}
+
/**
* Decodes a huffman encoded string into the target StringBuilder. There
must be enough space left in the buffer for
* this method to succeed.
*
- * @param data The byte buffer
- * @param length The length of data from the buffer to decode
- * @param target The target for the decompressed data
- * @param isFieldName {@code true} if a field name is being decoded (names
have a more restrictive set of allowed
- * characters than field values)
+ * @param data The byte buffer
+ * @param length The length of data from the buffer to decode
+ * @param target The target for the decompressed data
*
* @throws HpackException If the Huffman encoded value in HPACK headers
did not end with EOS padding
*/
- public static void decode(ByteBuffer data, int length, StringBuilder
target, boolean isFieldName)
- throws HpackException {
+ public static void decode(ByteBuffer data, int length, StringBuilder
target) throws HpackException {
assert data.remaining() >= length;
int treePos = 0;
boolean eosBits = true;
int eosBitCount = 0;
- boolean firstChar = true;
- char c = 'a';
for (int i = 0; i < length; ++i) {
byte b = data.get();
int bitPos = 7;
@@ -427,27 +426,7 @@ public class HPackHuffman {
// Found a zero, can't be counting EOS bits
eosBitCount = 0;
} else {
- c = (char) (val & LOW_MASK);
- if (isFieldName) {
- if (!HttpParser.isToken(c) ||
Character.isUpperCase(c)) {
- throw new IllegalArgumentException(sm
-
.getString("hpackhuffman.decode.illegalCharacterName", Character.toString(c)));
- }
- } else {
- if (firstChar) {
- if (!HttpParser.isFieldVChar(c)) {
- throw new
IllegalArgumentException(sm.getString(
-
"hpackhuffman.decode.illegalCharacterValue.start", Character.toString(c)));
- }
- firstChar = false;
- } else {
- if (!HttpParser.isFieldContent(c)) {
- throw new
IllegalArgumentException(sm.getString(
-
"hpackhuffman.decode.illegalCharacterValue", Character.toString(c)));
- }
- }
- }
- target.append(c);
+ target.append((char) (val & LOW_MASK));
treePos = 0;
eosBits = true;
// Output a character, reset eosBitCount
@@ -466,27 +445,7 @@ public class HPackHuffman {
// as an error
throw new
HpackException(sm.getString("hpackhuffman.stringLiteralEOS"));
}
- c = (char) ((val >> 16) & LOW_MASK);
- if (isFieldName) {
- if (!HttpParser.isToken(c) ||
Character.isUpperCase(c)) {
- throw new IllegalArgumentException(sm
-
.getString("hpackhuffman.decode.illegalCharacterName", Character.toString(c)));
- }
- } else {
- if (firstChar) {
- if (!HttpParser.isFieldVChar(c)) {
- throw new
IllegalArgumentException(sm.getString(
-
"hpackhuffman.decode.illegalCharacterValue.start", Character.toString(c)));
- }
- firstChar = false;
- } else {
- if (!HttpParser.isFieldContent(c)) {
- throw new
IllegalArgumentException(sm.getString(
-
"hpackhuffman.decode.illegalCharacterValue", Character.toString(c)));
- }
- }
- }
- target.append(c);
+ target.append((char) ((val >> 16) & LOW_MASK));
treePos = 0;
eosBits = true;
// Output a character, reset eosBitCount
@@ -502,10 +461,6 @@ public class HPackHuffman {
if (!eosBits) {
throw new
HpackException(sm.getString("hpackhuffman.huffmanEncodedHpackValueDidNotEndWithEOS"));
}
- if (!isFieldName && !HttpParser.isFieldVChar(c)) {
- throw new IllegalArgumentException(
-
sm.getString("hpackhuffman.decode.illegalCharacterValue.end",
Character.toString(c)));
- }
}
diff --git a/java/org/apache/coyote/http2/HpackDecoder.java
b/java/org/apache/coyote/http2/HpackDecoder.java
index b914523293..5e7fd071bc 100644
--- a/java/org/apache/coyote/http2/HpackDecoder.java
+++ b/java/org/apache/coyote/http2/HpackDecoder.java
@@ -21,7 +21,6 @@ import java.util.concurrent.atomic.AtomicReference;
import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
-import org.apache.tomcat.util.http.parser.HttpParser;
import org.apache.tomcat.util.res.StringManager;
/**
@@ -116,7 +115,7 @@ public class HpackDecoder {
buffer.position(originalPos);
return;
}
- String headerValue = readHpackString(buffer, false);
+ String headerValue = readHpackString(buffer);
if (headerValue == null) {
buffer.position(originalPos);
return;
@@ -130,7 +129,7 @@ public class HpackDecoder {
buffer.position(originalPos);
return;
}
- String headerValue = readHpackString(buffer, false);
+ String headerValue = readHpackString(buffer);
if (headerValue == null) {
buffer.position(originalPos);
return;
@@ -143,7 +142,7 @@ public class HpackDecoder {
buffer.position(originalPos);
return;
}
- String headerValue = readHpackString(buffer, false);
+ String headerValue = readHpackString(buffer);
if (headerValue == null) {
buffer.position(originalPos);
return;
@@ -205,11 +204,11 @@ public class HpackDecoder {
} else if (index != 0) {
return handleIndexedHeaderName(index);
} else {
- return readHpackString(buffer, true);
+ return readHpackString(buffer);
}
}
- private String readHpackString(ByteBuffer buffer, boolean isFieldName)
throws HpackException {
+ private String readHpackString(ByteBuffer buffer) throws HpackException {
if (!buffer.hasRemaining()) {
return null;
}
@@ -221,34 +220,18 @@ public class HpackDecoder {
}
boolean huffman = (data & 0b10000000) != 0;
if (huffman) {
- return readHuffmanString(length, buffer, isFieldName);
+ return readHuffmanString(length, buffer);
}
StringBuilder stringBuilder = new StringBuilder(length);
for (int i = 0; i < length; ++i) {
- char c = (char) (buffer.get() & 0xFF);
- if (isFieldName) {
- if (HttpParser.isToken(c) && !Character.isUpperCase(c)) {
- stringBuilder.append(c);
- } else {
- throw new IllegalArgumentException(
- sm.getString("hpackdecoder.illegalCharacterName",
Character.toString(c)));
- }
- } else {
- if ((i == 0 || i == length - 1) && HttpParser.isFieldVChar(c)
||
- i > 0 && i < length - 1 &&
HttpParser.isFieldContent(c)) {
- stringBuilder.append(c);
- } else {
- throw new IllegalArgumentException(
- sm.getString("hpackdecoder.illegalCharacterValue",
Character.toString(c)));
- }
- }
+ stringBuilder.append((char) (buffer.get() & 0xFF));
}
return stringBuilder.toString();
}
- private String readHuffmanString(int length, ByteBuffer buffer, boolean
isFieldName) throws HpackException {
+ private String readHuffmanString(int length, ByteBuffer buffer) throws
HpackException {
StringBuilder stringBuilder = new StringBuilder(length);
- HPackHuffman.decode(buffer, length, stringBuilder, isFieldName);
+ HPackHuffman.decode(buffer, length, stringBuilder);
return stringBuilder.toString();
}
diff --git a/java/org/apache/coyote/http2/Http2Parser.java
b/java/org/apache/coyote/http2/Http2Parser.java
index 8b26c99bbe..f389e3c670 100644
--- a/java/org/apache/coyote/http2/Http2Parser.java
+++ b/java/org/apache/coyote/http2/Http2Parser.java
@@ -570,8 +570,6 @@ class Http2Parser {
} catch (HpackException hpe) {
throw new
ConnectionException(sm.getString("http2Parser.processFrameHeaders.decodingFailed"),
Http2Error.COMPRESSION_ERROR, hpe);
- } catch (IllegalArgumentException iae) {
- throw new StreamException("Invalid headers",
Http2Error.PROTOCOL_ERROR, streamId, iae);
}
// switches to write mode
diff --git a/java/org/apache/coyote/http2/LocalStrings.properties
b/java/org/apache/coyote/http2/LocalStrings.properties
index 028f26fb25..d59fd26a68 100644
--- a/java/org/apache/coyote/http2/LocalStrings.properties
+++ b/java/org/apache/coyote/http2/LocalStrings.properties
@@ -46,8 +46,6 @@ hpackdecoder.addDynamic=Adding header to index [{0}] of
dynamic table with name
hpackdecoder.clearDynamic=Emptying dynamic table
hpackdecoder.emitHeader=Emitting header with name [{0}] and value [{1}]
hpackdecoder.headerTableIndexInvalid=The header table index [{0}] is not valid
as there are [{1}] static entries and [{2}] dynamic entries
-hpackdecoder.illegalCharacterName=The illegal [{0}] character was found when
decoding an HTTP/2 header field name
-hpackdecoder.illegalCharacterValue=The illegal [{0}] character was found when
decoding an HTTP/2 header field value
hpackdecoder.maxMemorySizeExceeded=The header table size [{0}] exceeds the
maximum size [{1}]
hpackdecoder.notImplemented=Not yet implemented
hpackdecoder.nullHeader=Null header at index [{0}]
@@ -56,10 +54,6 @@ hpackdecoder.useDynamic=Using header from index [{0}] of
dynamic table
hpackdecoder.useStatic=Using header from index [{0}] of static table
hpackdecoder.zeroNotValidHeaderTableIndex=Zero is not a valid header table
index
-hpackhuffman.decode.illegalCharacterName=The illegal [{0}] character was found
when decoding an HTTP/2 header field name
-hpackhuffman.decode.illegalCharacterValue=The illegal [{0}] character was
found when decoding an HTTP/2 header field value
-hpackhuffman.decode.illegalCharacterValue.end=The illegal [{0}] character was
found when decoding the final character in an HTTP/2 header field value
-hpackhuffman.decode.illegalCharacterValue.start=The illegal [{0}] character
was found when decoding the first character in an HTTP/2 header field value
hpackhuffman.huffmanEncodedHpackValueDidNotEndWithEOS=Huffman encoded value in
HPACK headers did not end with EOS padding
hpackhuffman.stringLiteralEOS=Huffman encoded value in HPACK headers contained
the EOS symbol
hpackhuffman.stringLiteralTooMuchPadding=More than 7 bits of EOS padding were
provided at the end of an Huffman encoded string literal
@@ -108,11 +102,15 @@ stream.header.empty=Connection [{0}], Stream [{1}],
Invalid empty header name
stream.header.inconsistentScheme=Connection [{0}], Stream [{1}], The scheme
[{2}] is not consistent with the TLS enabled setting of [{3}]
stream.header.invalid=Connection [{0}], Stream [{1}], The header [{2}]
contained invalid value [{3}]
stream.header.invalidConnect=Connection [{0}], Stream [{1}], The CONNECT
request was invalid as neither :scheme nor :path should be present
+stream.header.name.invalidCharacter=Connection [{0}], Stream [{1}], The
illegal character [{2}] was found when decoding an HTTP/2 header field name
[{3}]
stream.header.noPath=Connection [{0}], Stream [{1}], The [:path] pseudo header
was empty
stream.header.required=Connection [{0}], Stream [{1}], One or more required
headers was missing
stream.header.te=Connection [{0}], Stream [{1}], HTTP header [te] is not
permitted to have the value [{2}] in an HTTP/2 request
stream.header.unexpectedPseudoHeader=Connection [{0}], Stream [{1}], Pseudo
header [{2}] received after a regular header
stream.header.unknownPseudoHeader=Connection [{0}], Stream [{1}], Unknown
pseudo header [{2}] received
+stream.header.value.invalidCharacter=Connection [{0}], Stream [{1}], The
illegal character [{2}] was found when decoding an HTTP/2 header field value
[{3}]
+stream.header.value.invalidCharacter.end=Connection [{0}], Stream [{1}], The
illegal final character [{2}] was found when decoding an HTTP/2 header field
value [{3}]
+stream.header.value.invalidCharacter.start=Connection [{0}], Stream [{1}], The
illegal first character [{2}] was found when decoding an HTTP/2 header field
value [{3}]
stream.host.inconsistent=Connection [{0}], Stream [{1}], The host header [{2}]
is inconsistent with previously provided values for host [{3}] and/or port [{4}]
stream.host.sni=Connection [{0}], Stream [{1}], The host header [{2}] does not
match the SNI host [{3}]
stream.inputBuffer.copy=Copying [{0}] bytes from inBuffer to outBuffer
diff --git a/java/org/apache/coyote/http2/Stream.java
b/java/org/apache/coyote/http2/Stream.java
index 33b223333d..9bbcb1c62c 100644
--- a/java/org/apache/coyote/http2/Stream.java
+++ b/java/org/apache/coyote/http2/Stream.java
@@ -52,6 +52,7 @@ import org.apache.tomcat.util.http.HeaderUtil;
import org.apache.tomcat.util.http.Method;
import org.apache.tomcat.util.http.MimeHeaders;
import org.apache.tomcat.util.http.parser.Host;
+import org.apache.tomcat.util.http.parser.HttpParser;
import org.apache.tomcat.util.http.parser.Priority;
import org.apache.tomcat.util.net.ApplicationBufferHandler;
import org.apache.tomcat.util.net.WriteBuffer;
@@ -331,7 +332,63 @@ class Stream extends AbstractNonZeroStream implements
HeaderEmitter {
log.trace(sm.getString("stream.header.debug", getConnectionId(),
getIdAsString(), name, value));
}
- // Field header names being all lower case is enforced in HpackDecoder.
+ // Validate field name
+ if (name.isEmpty()) {
+ headerException =
+ new StreamException(sm.getString("stream.header.empty",
getConnectionId(), getIdAsString()),
+ Http2Error.PROTOCOL_ERROR, getIdAsInt());
+ // No need for further processing. The stream will be reset.
+ return;
+ }
+ for (int i = 0; i < name.length(); i++) {
+ char c = name.charAt(i);
+ // Skip pseudo headers
+ if (i == 0 && c == ':') {
+ continue;
+ }
+ if (!HttpParser.isToken(c) || Character.isUpperCase(c)) {
+ headerException =
+ new
StreamException(sm.getString("stream.header.name.invalidCharacter",
getConnectionId(),
+ getIdAsString(), Character.toString(c), name),
Http2Error.PROTOCOL_ERROR, getIdAsInt());
+ // No need for further processing. The stream will be reset.
+ return;
+ }
+ }
+
+ // Validate field value
+ for (int i = 0; i < value.length(); i++) {
+ char c = value.charAt(i);
+ if (i == 0) {
+ if (!HttpParser.isFieldVChar(c)) {
+ headerException = new StreamException(
+
sm.getString("stream.header.value.invalidCharacter.start", getConnectionId(),
+ getIdAsString(), Character.toString(c),
value),
+ Http2Error.PROTOCOL_ERROR, getIdAsInt());
+ // No need for further processing. The stream will be
reset.
+ return;
+ }
+ } else if (i == value.length() - 1) {
+ if (!HttpParser.isFieldVChar(c)) {
+ headerException = new StreamException(
+
sm.getString("stream.header.value.invalidCharacter.end", getConnectionId(),
+ getIdAsString(), Character.toString(c),
value),
+ Http2Error.PROTOCOL_ERROR, getIdAsInt());
+ // No need for further processing. The stream will be
reset.
+ return;
+ }
+ } else {
+ if (!HttpParser.isFieldContent(c)) {
+ headerException =
+ new StreamException(
+
sm.getString("stream.header.value.invalidCharacter", getConnectionId(),
+ getIdAsString(),
Character.toString(c), value),
+ Http2Error.PROTOCOL_ERROR, getIdAsInt());
+ // No need for further processing. The stream will be
reset.
+ return;
+ }
+ }
+ }
+
if (HTTP_CONNECTION_SPECIFIC_HEADERS.contains(name)) {
headerException = new StreamException(
@@ -357,14 +414,6 @@ class Stream extends AbstractNonZeroStream implements
HeaderEmitter {
return;
}
- if (name.isEmpty()) {
- headerException =
- new StreamException(sm.getString("stream.header.empty",
getConnectionId(), getIdAsString()),
- Http2Error.PROTOCOL_ERROR, getIdAsInt());
- // No need for further processing. The stream will be reset.
- return;
- }
-
boolean pseudoHeader = name.charAt(0) == ':';
if (pseudoHeader && headerState != HEADER_STATE_PSEUDO) {
diff --git a/test/org/apache/coyote/http2/TestHPackHuffman.java
b/test/org/apache/coyote/http2/TestHPackHuffman.java
index 1dc457c506..5aaf6ac8f9 100644
--- a/test/org/apache/coyote/http2/TestHPackHuffman.java
+++ b/test/org/apache/coyote/http2/TestHPackHuffman.java
@@ -38,7 +38,7 @@ public class TestHPackHuffman {
buf.get();
StringBuilder target = new StringBuilder();
- HPackHuffman.decode(buf, buf.remaining(), target, false);
+ HPackHuffman.decode(buf, buf.remaining(), target);
Assert.assertEquals("Value changed after encode/decode roundtrip",
data, target.toString());
}
diff --git a/test/org/apache/coyote/http2/TestHttp2Section_4_3.java
b/test/org/apache/coyote/http2/TestHttp2Section_4_3.java
index 0fbcfa5c35..48798b3cd1 100644
--- a/test/org/apache/coyote/http2/TestHttp2Section_4_3.java
+++ b/test/org/apache/coyote/http2/TestHttp2Section_4_3.java
@@ -17,16 +17,23 @@
package org.apache.coyote.http2;
import java.nio.ByteBuffer;
+import java.util.ArrayList;
+import java.util.List;
import org.junit.Assert;
import org.junit.Test;
+import org.apache.tomcat.util.http.Method;
+
/**
* Unit tests for Section 4.3 of <a
href="https://tools.ietf.org/html/rfc7540">RFC 7540</a>. <br>
* The order of tests in this class is aligned with the order of the
requirements in the RFC.
*/
public class TestHttp2Section_4_3 extends Http2TestBase {
+ private static final String MARKER_HEADER_NAME = "x-marker";
+ private static final String MARKER_HEADER_VALUE = "marker-value";
+
@Test
public void testHeaderDecodingError() throws Exception {
// HTTP2 upgrade
@@ -47,6 +54,160 @@ public class TestHttp2Section_4_3 extends Http2TestBase {
}
+ /*
+ * HTTP/2 field validation rejects invalid characters in field names and
field values. The field name check has two
+ * distinct ways to fail (not a token character; a valid but upper case
token character) and the field value check
+ * has three distinct ways to fail depending on the position of the
invalid character (first, middle, last). Each
+ * of those five triggers has tests below for both the non-Huffman and
Huffman code paths. HpackEncoder uses Huffman
+ * encoding for strings longer than five characters, provided that the
encoded form is not longer than the original.
+ */
+
+ @Test
+ public void testHeaderDecodingErrorFieldNameInvalidCharacter() throws
Exception {
+ // ':' is a separator so is not a valid token character.
+ doTestHeaderDecodingErrorKeepsTableInSync("x:y", "ok");
+ }
+
+
+ @Test
+ public void testHeaderDecodingErrorFieldNameUpperCase() throws Exception {
+ // Upper case letters are valid token characters but are not permitted
in HTTP/2 header field names.
+ doTestHeaderDecodingErrorKeepsTableInSync("Xy", "ok");
+ }
+
+
+ @Test
+ public void testHeaderDecodingErrorFieldValueLeadingInvalidCharacter()
throws Exception {
+ // The first character of a field value must be a field-vchar. Space
is not.
+ doTestHeaderDecodingErrorKeepsTableInSync("x-bad", " x");
+ }
+
+
+ @Test
+ public void testHeaderDecodingErrorFieldValueTrailingInvalidCharacter()
throws Exception {
+ // The last character of a field value must be a field-vchar. Space is
not.
+ doTestHeaderDecodingErrorKeepsTableInSync("x-bad", "x ");
+ }
+
+
+ @Test
+ public void testHeaderDecodingErrorFieldValueMiddleInvalidCharacter()
throws Exception {
+ // A character in the middle of a field value must be field-content. A
control character is not.
+ doTestHeaderDecodingErrorKeepsTableInSync("x-bad", "a\u0001b");
+ }
+
+
+ @Test
+ public void testHeaderDecodingErrorHuffmanFieldNameTrailingSpace() throws
Exception {
+ // Space is not a valid token character in a field name.
+ doTestHeaderDecodingErrorKeepsTableInSync("x-bad ", "ok");
+ }
+
+
+ @Test
+ public void testHeaderDecodingErrorHuffmanFieldNameInvalidCharacter()
throws Exception {
+ // ':' is a separator so is not a valid token character.
+ doTestHeaderDecodingErrorKeepsTableInSync("x-te:st", "ok");
+ }
+
+
+ @Test
+ public void testHeaderDecodingErrorHuffmanFieldNameUpperCase() throws
Exception {
+ // Upper case letters are valid token characters but are not permitted
in HTTP/2 header field names.
+ doTestHeaderDecodingErrorKeepsTableInSync("x-Te-st", "ok");
+ }
+
+
+ @Test
+ public void
testHeaderDecodingErrorHuffmanFieldValueLeadingInvalidCharacter() throws
Exception {
+ // The first character of a field value must be a field-vchar. Space
is not.
+ doTestHeaderDecodingErrorKeepsTableInSync("x-bad", " x-value");
+ }
+
+
+ @Test
+ public void
testHeaderDecodingErrorHuffmanFieldValueTrailingInvalidCharacter() throws
Exception {
+ // The last character of a field value must be a field-vchar. Space is
not.
+ doTestHeaderDecodingErrorKeepsTableInSync("x-bad", "x-value ");
+ }
+
+
+ @Test
+ public void
testHeaderDecodingErrorHuffmanFieldValueMiddleInvalidCharacter() throws
Exception {
+ // A character in the middle of a field value must be field-content. A
control character is not.
+ doTestHeaderDecodingErrorKeepsTableInSync("x-bad",
"x-value-test-\u0001-value-test");
+ }
+
+
+ /**
+ * A header field ({@code badHeaderName} / {@code badHeaderValue}) that
{@link Stream#emitHeader(String, String)}
+ * rejects is a stream error (RFC 9113, section 8.2.1), not a connection
error, so the connection must remain open
+ * and usable. However, the invalid field is followed, in the same header
block, by another header
+ * ({@link #MARKER_HEADER_NAME}) that adds an entry to the HPACK dynamic
table. The decoder must still process that
+ * later header (i.e. keep going after detecting the invalid field) so its
dynamic table stays in sync with the
+ * encoder used by the client. This is verified with a second, otherwise
unrelated, request that references that
+ * dynamic table entry.
+ *
+ * @param badHeaderName The (possibly invalid) name to use for the
invalid header
+ * @param badHeaderValue The (possibly invalid) value to use for the
invalid header
+ */
+ private void doTestHeaderDecodingErrorKeepsTableInSync(String
badHeaderName, String badHeaderValue)
+ throws Exception {
+ // HTTP2 upgrade
+ http2Connect();
+
+ byte[] frameHeader = new byte[9];
+ ByteBuffer headersPayload = ByteBuffer.allocate(128);
+
+ List<Header> headers = new ArrayList<>(6);
+ headers.add(new Header(":method", Method.GET));
+ headers.add(new Header(":scheme", "http"));
+ headers.add(new Header(":path", "/simple"));
+ headers.add(new Header(":authority", "localhost:" + getPort()));
+ headers.add(new Header(badHeaderName, badHeaderValue));
+ // Valid header that follows the invalid one in the same header
+ // block. It must still be added to the dynamic table.
+ headers.add(new Header(MARKER_HEADER_NAME, MARKER_HEADER_VALUE));
+
+ buildGetRequest(frameHeader, headersPayload, null, headers, 3);
+ writeFrame(frameHeader, headersPayload);
+
+ // The stream must be reset. The connection must remain open.
+ parser.readFrame();
+ Assert.assertEquals("Stream (not connection) error expected for an
invalid header field",
+ "3-RST-[" + Http2Error.PROTOCOL_ERROR.getCode() + "]\n",
output.getTrace());
+ output.clearTrace();
+
+ // A second, unrelated request on a new stream. Because it uses the
+ // same name/value pair as the marker header above, the test's
+ // HpackEncoder (correctly simulating a real HTTP/2 client) will
+ // reference its dynamic table entry rather than re-sending it as a
+ // literal. This will only decode correctly on the server if the
+ // HPACK decoder kept processing the earlier header block far enough
+ // to add that entry to its own dynamic table, despite the stream
+ // being reset.
+ byte[] frameHeader2 = new byte[9];
+ ByteBuffer headersPayload2 = ByteBuffer.allocate(128);
+
+ List<Header> headers2 = new ArrayList<>(5);
+ headers2.add(new Header(":method", Method.GET));
+ headers2.add(new Header(":scheme", "http"));
+ headers2.add(new Header(":path", "/simple"));
+ headers2.add(new Header(":authority", "localhost:" + getPort()));
+ headers2.add(new Header(MARKER_HEADER_NAME, MARKER_HEADER_VALUE));
+
+ buildGetRequest(frameHeader2, headersPayload2, null, headers2, 5);
+ writeFrame(frameHeader2, headersPayload2);
+
+ parser.readFrame();
+ Assert.assertFalse(output.getTrace(),
output.getTrace().contains("RST"));
+ parser.readFrame();
+
+ Assert.assertEquals("HPACK dynamic table should still be in sync with
the client",
+ getSimpleResponseTrace(5), output.getTrace());
+ }
+
+
@Test
public void testHeaderContinuationContiguous() throws Exception {
// HTTP2 upgrade
@@ -65,6 +226,7 @@ public class TestHttp2Section_4_3 extends Http2TestBase {
// headers, body
parser.readFrame();
+ Assert.assertFalse(output.getTrace(),
output.getTrace().contains("RST"));
parser.readFrame();
Assert.assertEquals(getSimpleResponseTrace(3), output.getTrace());
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 53543b7a62..d159753fc9 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -260,6 +260,11 @@
<fix>
Additional clean-up after HTTP/2 stream reset to aid GC. (markt)
</fix>
+ <fix>
+ Revert earlier refactoring of HTTP/2 header field validation that moved
+ it earlier since the refactoring made correct handling of invalid
+ headers more difficult. (markt)
+ </fix>
</changelog>
</subsection>
<subsection name="Jasper">
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]