Skip to content

Commit 3e6b54b

Browse files
Julien HoarauScottmitch
Julien Hoarau
authored andcommitted
Fix failing h2spec tests 8.1.2.1 related to pseudo-headers validation
Motivation: According to the spec: All pseudo-header fields MUST appear in the header block before regular header fields. Any request or response that contains a pseudo-header field that appears in a header block after a regular header field MUST be treated as malformed (Section 8.1.2.6). Pseudo-header fields are only valid in the context in which they are defined. Pseudo-header fields defined for requests MUST NOT appear in responses; pseudo-header fields defined for responses MUST NOT appear in requests. Pseudo-header fields MUST NOT appear in trailers. Endpoints MUST treat a request or response that contains undefined or invalid pseudo-header fields as malformed (Section 8.1.2.6). Clients MUST NOT accept a malformed response. Note that these requirements are intended to protect against several types of common attacks against HTTP; they are deliberately strict because being permissive can expose implementations to these vulnerabilities. Modifications: - Introduce validation in HPackDecoder Result: - Requests with unknown pseudo-field headers are rejected - Requests with containing response specific pseudo-headers are rejected - Requests where pseudo-header appear after regular header are rejected - h2spec 8.1.2.1 pass
1 parent c795e88 commit 3e6b54b

File tree

12 files changed

+361
-51
lines changed

12 files changed

+361
-51
lines changed

codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Headers.java

+8-5
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,19 @@
2121
import io.netty.util.internal.PlatformDependent;
2222
import io.netty.util.internal.UnstableApi;
2323

24-
import static io.netty.handler.codec.http2.Http2Error.*;
25-
import static io.netty.handler.codec.http2.Http2Exception.*;
26-
import static io.netty.util.AsciiString.*;
24+
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
25+
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
26+
import static io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName.hasPseudoHeaderFormat;
27+
import static io.netty.util.AsciiString.CASE_INSENSITIVE_HASHER;
28+
import static io.netty.util.AsciiString.CASE_SENSITIVE_HASHER;
29+
import static io.netty.util.AsciiString.isUpperCase;
2730

2831
@UnstableApi
2932
public class DefaultHttp2Headers
3033
extends DefaultHeaders<CharSequence, CharSequence, Http2Headers> implements Http2Headers {
3134
private static final ByteProcessor HTTP2_NAME_VALIDATOR_PROCESSOR = new ByteProcessor() {
3235
@Override
33-
public boolean process(byte value) throws Exception {
36+
public boolean process(byte value) {
3437
return !isUpperCase(value);
3538
}
3639
};
@@ -207,7 +210,7 @@ protected Http2HeaderEntry(int hash, CharSequence key, CharSequence value,
207210
this.next = next;
208211

209212
// Make sure the pseudo headers fields are first in iteration order
210-
if (key.length() != 0 && key.charAt(0) == ':') {
213+
if (hasPseudoHeaderFormat(key)) {
211214
after = firstNonPseudo;
212215
before = firstNonPseudo.before();
213216
} else {

codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ public Configuration configuration() {
115115
public Http2Headers decodeHeaders(int streamId, ByteBuf headerBlock) throws Http2Exception {
116116
try {
117117
final Http2Headers headers = newHeaders();
118-
hpackDecoder.decode(streamId, headerBlock, headers);
118+
hpackDecoder.decode(streamId, headerBlock, headers, validateHeaders);
119119
headerArraySizeAccumulator = HEADERS_COUNT_WEIGHT_NEW * headers.size() +
120120
HEADERS_COUNT_WEIGHT_HISTORICAL * headerArraySizeAccumulator;
121121
return headers;

codec-http2/src/main/java/io/netty/handler/codec/http2/HpackDecoder.java

+57-8
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@
4545
import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
4646
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
4747
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
48+
import static io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName.getPseudoHeader;
49+
import static io.netty.handler.codec.http2.Http2Headers.PseudoHeaderName.hasPseudoHeaderFormat;
4850
import static io.netty.util.AsciiString.EMPTY_STRING;
4951
import static io.netty.util.internal.ObjectUtil.checkPositive;
5052
import static io.netty.util.internal.ThrowableUtil.unknownStackTrace;
@@ -119,14 +121,15 @@ final class HpackDecoder {
119121
* <p>
120122
* This method assumes the entire header block is contained in {@code in}.
121123
*/
122-
public void decode(int streamId, ByteBuf in, Http2Headers headers) throws Http2Exception {
124+
public void decode(int streamId, ByteBuf in, Http2Headers headers, boolean validateHeaders) throws Http2Exception {
123125
int index = 0;
124126
long headersLength = 0;
125127
int nameLength = 0;
126128
int valueLength = 0;
127129
byte state = READ_HEADER_REPRESENTATION;
128130
boolean huffmanEncoded = false;
129131
CharSequence name = null;
132+
HeaderType headerType = null;
130133
IndexType indexType = IndexType.NONE;
131134
while (in.isReadable()) {
132135
switch (state) {
@@ -146,7 +149,10 @@ public void decode(int streamId, ByteBuf in, Http2Headers headers) throws Http2E
146149
state = READ_INDEXED_HEADER;
147150
break;
148151
default:
149-
headersLength = indexHeader(index, headers, headersLength);
152+
HpackHeaderField indexedHeader = getIndexedHeader(index);
153+
headerType = validate(indexedHeader.name, headerType, validateHeaders);
154+
headersLength = addHeader(headers, indexedHeader.name, indexedHeader.value,
155+
headersLength);
150156
}
151157
} else if ((b & 0x40) == 0x40) {
152158
// Literal Header Field with Incremental Indexing
@@ -162,6 +168,7 @@ public void decode(int streamId, ByteBuf in, Http2Headers headers) throws Http2E
162168
default:
163169
// Index was stored as the prefix
164170
name = readName(index);
171+
headerType = validate(name, headerType, validateHeaders);
165172
nameLength = name.length();
166173
state = READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;
167174
}
@@ -188,6 +195,7 @@ public void decode(int streamId, ByteBuf in, Http2Headers headers) throws Http2E
188195
default:
189196
// Index was stored as the prefix
190197
name = readName(index);
198+
headerType = validate(name, headerType, validateHeaders);
191199
nameLength = name.length();
192200
state = READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;
193201
}
@@ -200,13 +208,16 @@ public void decode(int streamId, ByteBuf in, Http2Headers headers) throws Http2E
200208
break;
201209

202210
case READ_INDEXED_HEADER:
203-
headersLength = indexHeader(decodeULE128(in, index), headers, headersLength);
211+
HpackHeaderField indexedHeader = getIndexedHeader(decodeULE128(in, index));
212+
headerType = validate(indexedHeader.name, headerType, validateHeaders);
213+
headersLength = addHeader(headers, indexedHeader.name, indexedHeader.value, headersLength);
204214
state = READ_HEADER_REPRESENTATION;
205215
break;
206216

207217
case READ_INDEXED_HEADER_NAME:
208218
// Header Name matches an entry in the Header Table
209219
name = readName(decodeULE128(in, index));
220+
headerType = validate(name, headerType, validateHeaders);
210221
nameLength = name.length();
211222
state = READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;
212223
break;
@@ -243,6 +254,7 @@ public void decode(int streamId, ByteBuf in, Http2Headers headers) throws Http2E
243254
}
244255

245256
name = readStringLiteral(in, nameLength, huffmanEncoded);
257+
headerType = validate(name, headerType, validateHeaders);
246258

247259
state = READ_LITERAL_HEADER_VALUE_LENGTH_PREFIX;
248260
break;
@@ -256,6 +268,7 @@ public void decode(int streamId, ByteBuf in, Http2Headers headers) throws Http2E
256268
state = READ_LITERAL_HEADER_VALUE_LENGTH;
257269
break;
258270
case 0:
271+
headerType = validate(name, headerType, validateHeaders);
259272
headersLength = insertHeader(headers, name, EMPTY_STRING, indexType, headersLength);
260273
state = READ_HEADER_REPRESENTATION;
261274
break;
@@ -288,6 +301,7 @@ public void decode(int streamId, ByteBuf in, Http2Headers headers) throws Http2E
288301
}
289302

290303
CharSequence value = readStringLiteral(in, valueLength, huffmanEncoded);
304+
headerType = validate(name, headerType, validateHeaders);
291305
headersLength = insertHeader(headers, name, value, indexType, headersLength);
292306
state = READ_HEADER_REPRESENTATION;
293307
break;
@@ -386,6 +400,34 @@ private void setDynamicTableSize(long dynamicTableSize) throws Http2Exception {
386400
hpackDynamicTable.setCapacity(dynamicTableSize);
387401
}
388402

403+
private HeaderType validate(CharSequence name, HeaderType previousHeaderType,
404+
final boolean validateHeaders) throws Http2Exception {
405+
if (!validateHeaders) {
406+
return null;
407+
}
408+
409+
if (hasPseudoHeaderFormat(name)) {
410+
if (previousHeaderType == HeaderType.REGULAR_HEADER) {
411+
throw connectionError(PROTOCOL_ERROR, "Pseudo-header field '%s' found after regular header.", name);
412+
}
413+
414+
final Http2Headers.PseudoHeaderName pseudoHeader = getPseudoHeader(name);
415+
if (pseudoHeader == null) {
416+
throw connectionError(PROTOCOL_ERROR, "Invalid HTTP/2 pseudo-header '%s' encountered.", name);
417+
}
418+
419+
final HeaderType currentHeaderType = pseudoHeader.isRequestOnly() ?
420+
HeaderType.REQUEST_PSEUDO_HEADER : HeaderType.RESPONSE_PSEUDO_HEADER;
421+
if (previousHeaderType != null && currentHeaderType != previousHeaderType) {
422+
throw connectionError(PROTOCOL_ERROR, "Mix of request and response pseudo-headers.");
423+
}
424+
425+
return currentHeaderType;
426+
}
427+
428+
return HeaderType.REGULAR_HEADER;
429+
}
430+
389431
private CharSequence readName(int index) throws Http2Exception {
390432
if (index <= HpackStaticTable.length) {
391433
HpackHeaderField hpackHeaderField = HpackStaticTable.getEntry(index);
@@ -398,14 +440,12 @@ private CharSequence readName(int index) throws Http2Exception {
398440
throw READ_NAME_ILLEGAL_INDEX_VALUE;
399441
}
400442

401-
private long indexHeader(int index, Http2Headers headers, long headersLength) throws Http2Exception {
443+
private HpackHeaderField getIndexedHeader(int index) throws Http2Exception {
402444
if (index <= HpackStaticTable.length) {
403-
HpackHeaderField hpackHeaderField = HpackStaticTable.getEntry(index);
404-
return addHeader(headers, hpackHeaderField.name, hpackHeaderField.value, headersLength);
445+
return HpackStaticTable.getEntry(index);
405446
}
406447
if (index - HpackStaticTable.length <= hpackDynamicTable.length()) {
407-
HpackHeaderField hpackHeaderField = hpackDynamicTable.getEntry(index - HpackStaticTable.length);
408-
return addHeader(headers, hpackHeaderField.name, hpackHeaderField.value, headersLength);
448+
return hpackDynamicTable.getEntry(index - HpackStaticTable.length);
409449
}
410450
throw INDEX_HEADER_ILLEGAL_INDEX_VALUE;
411451
}
@@ -504,4 +544,13 @@ static long decodeULE128(ByteBuf in, long result) throws Http2Exception {
504544

505545
throw DECODE_ULE_128_DECOMPRESSION_EXCEPTION;
506546
}
547+
548+
/**
549+
* HTTP/2 header types.
550+
*/
551+
private enum HeaderType {
552+
REGULAR_HEADER,
553+
REQUEST_PSEUDO_HEADER,
554+
RESPONSE_PSEUDO_HEADER
555+
}
507556
}

codec-http2/src/main/java/io/netty/handler/codec/http2/Http2Headers.java

+46-8
Original file line numberDiff line numberDiff line change
@@ -35,51 +35,89 @@ enum PseudoHeaderName {
3535
/**
3636
* {@code :method}.
3737
*/
38-
METHOD(":method"),
38+
METHOD(":method", true),
3939

4040
/**
4141
* {@code :scheme}.
4242
*/
43-
SCHEME(":scheme"),
43+
SCHEME(":scheme", true),
4444

4545
/**
4646
* {@code :authority}.
4747
*/
48-
AUTHORITY(":authority"),
48+
AUTHORITY(":authority", true),
4949

5050
/**
5151
* {@code :path}.
5252
*/
53-
PATH(":path"),
53+
PATH(":path", true),
5454

5555
/**
5656
* {@code :status}.
5757
*/
58-
STATUS(":status");
58+
STATUS(":status", false);
59+
60+
private static final char PSEUDO_HEADER_PREFIX = ':';
61+
private static final byte PSEUDO_HEADER_PREFIX_BYTE = (byte) PSEUDO_HEADER_PREFIX;
5962

6063
private final AsciiString value;
61-
private static final CharSequenceMap<AsciiString> PSEUDO_HEADERS = new CharSequenceMap<AsciiString>();
64+
private final boolean requestOnly;
65+
private static final CharSequenceMap<PseudoHeaderName> PSEUDO_HEADERS = new CharSequenceMap<PseudoHeaderName>();
66+
6267
static {
6368
for (PseudoHeaderName pseudoHeader : PseudoHeaderName.values()) {
64-
PSEUDO_HEADERS.add(pseudoHeader.value(), AsciiString.EMPTY_STRING);
69+
PSEUDO_HEADERS.add(pseudoHeader.value(), pseudoHeader);
6570
}
6671
}
6772

68-
PseudoHeaderName(String value) {
73+
PseudoHeaderName(String value, boolean requestOnly) {
6974
this.value = AsciiString.cached(value);
75+
this.requestOnly = requestOnly;
7076
}
7177

7278
public AsciiString value() {
7379
// Return a slice so that the buffer gets its own reader index.
7480
return value;
7581
}
7682

83+
/**
84+
* Indicates whether the specified header follows the pseudo-header format (begins with ':' character)
85+
*
86+
* @return {@code true} if the header follow the pseudo-header format
87+
*/
88+
public static boolean hasPseudoHeaderFormat(CharSequence headerName) {
89+
if (headerName instanceof AsciiString) {
90+
final AsciiString asciiHeaderName = (AsciiString) headerName;
91+
return asciiHeaderName.length() > 0 && asciiHeaderName.byteAt(0) == PSEUDO_HEADER_PREFIX_BYTE;
92+
} else {
93+
return headerName.length() > 0 && headerName.charAt(0) == PSEUDO_HEADER_PREFIX;
94+
}
95+
}
96+
7797
/**
7898
* Indicates whether the given header name is a valid HTTP/2 pseudo header.
7999
*/
80100
public static boolean isPseudoHeader(CharSequence header) {
81101
return PSEUDO_HEADERS.contains(header);
82102
}
103+
104+
/**
105+
* Returns the {@link PseudoHeaderName} corresponding to the specified header name.
106+
*
107+
* @return corresponding {@link PseudoHeaderName} if any, {@code null} otherwise.
108+
*/
109+
public static PseudoHeaderName getPseudoHeader(CharSequence header) {
110+
return PSEUDO_HEADERS.get(header);
111+
}
112+
113+
/**
114+
* Indicates whether the pseudo-header is to be used in a request context.
115+
*
116+
* @return {@code true} if the pseudo-header is to be used in a request context
117+
*/
118+
public boolean isRequestOnly() {
119+
return requestOnly;
120+
}
83121
}
84122

85123
/**

0 commit comments

Comments
 (0)