Skip to content

Commit c94a7ae

Browse files
committed
8354276: Strict HTTP header validation
Reviewed-by: dfuchs, jpai
1 parent a5f4366 commit c94a7ae

File tree

6 files changed

+264
-21
lines changed

6 files changed

+264
-21
lines changed

src/java.net.http/share/classes/jdk/internal/net/http/Http2Connection.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
import jdk.internal.net.http.common.SequentialScheduler;
7070
import jdk.internal.net.http.common.Utils;
7171
import jdk.internal.net.http.common.ValidatingHeadersConsumer;
72+
import jdk.internal.net.http.common.ValidatingHeadersConsumer.Context;
7273
import jdk.internal.net.http.frame.ContinuationFrame;
7374
import jdk.internal.net.http.frame.DataFrame;
7475
import jdk.internal.net.http.frame.ErrorFrame;
@@ -89,7 +90,6 @@
8990
import jdk.internal.net.http.hpack.DecodingCallback;
9091
import jdk.internal.net.http.hpack.Encoder;
9192
import static java.nio.charset.StandardCharsets.UTF_8;
92-
import static jdk.internal.net.http.frame.SettingsFrame.DEFAULT_INITIAL_WINDOW_SIZE;
9393
import static jdk.internal.net.http.frame.SettingsFrame.ENABLE_PUSH;
9494
import static jdk.internal.net.http.frame.SettingsFrame.HEADER_TABLE_SIZE;
9595
import static jdk.internal.net.http.frame.SettingsFrame.INITIAL_CONNECTION_WINDOW_SIZE;
@@ -340,6 +340,7 @@ private final class PushPromiseDecoder extends HeaderDecoder implements Decoding
340340
final AtomicReference<Throwable> errorRef = new AtomicReference<>();
341341

342342
PushPromiseDecoder(int parentStreamId, int pushPromiseStreamId, Stream<?> parent) {
343+
super(Context.REQUEST);
343344
this.parentStreamId = parentStreamId;
344345
this.pushPromiseStreamId = pushPromiseStreamId;
345346
this.parent = parent;
@@ -984,7 +985,10 @@ void processFrame(Http2Frame frame) throws IOException {
984985
// always decode the headers as they may affect
985986
// connection-level HPACK decoding state
986987
if (orphanedConsumer == null || frame.getClass() != ContinuationFrame.class) {
987-
orphanedConsumer = new ValidatingHeadersConsumer();
988+
orphanedConsumer = new ValidatingHeadersConsumer(
989+
frame instanceof PushPromiseFrame ?
990+
Context.REQUEST :
991+
Context.RESPONSE);
988992
}
989993
DecodingCallback decoder = orphanedConsumer::onDecoded;
990994
try {

src/java.net.http/share/classes/jdk/internal/net/http/Stream.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1871,7 +1871,12 @@ void closeAsUnprocessed() {
18711871
}
18721872
}
18731873

1874-
private class HeadersConsumer extends ValidatingHeadersConsumer implements DecodingCallback {
1874+
private final class HeadersConsumer extends ValidatingHeadersConsumer
1875+
implements DecodingCallback {
1876+
1877+
private HeadersConsumer() {
1878+
super(Context.RESPONSE);
1879+
}
18751880

18761881
boolean maxHeaderListSizeReached;
18771882

src/java.net.http/share/classes/jdk/internal/net/http/common/HeaderDecoder.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -30,7 +30,8 @@ public class HeaderDecoder extends ValidatingHeadersConsumer {
3030

3131
private final HttpHeadersBuilder headersBuilder;
3232

33-
public HeaderDecoder() {
33+
public HeaderDecoder(Context context) {
34+
super(context);
3435
this.headersBuilder = new HttpHeadersBuilder();
3536
}
3637

src/java.net.http/share/classes/jdk/internal/net/http/common/ValidatingHeadersConsumer.java

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -26,15 +26,39 @@
2626

2727
import java.io.IOException;
2828
import java.io.UncheckedIOException;
29+
import java.net.ProtocolException;
30+
import java.util.Map;
31+
import java.util.Objects;
2932
import java.util.Set;
3033

3134
/*
3235
* Checks RFC 9113 rules (relaxed) compliance regarding pseudo-headers.
3336
*/
3437
public class ValidatingHeadersConsumer {
3538

36-
private static final Set<String> PSEUDO_HEADERS =
37-
Set.of(":authority", ":method", ":path", ":scheme", ":status");
39+
private final Context context;
40+
41+
public ValidatingHeadersConsumer(Context context) {
42+
this.context = Objects.requireNonNull(context);
43+
}
44+
45+
public enum Context {
46+
REQUEST,
47+
RESPONSE,
48+
}
49+
50+
// Map of permitted pseudo headers in requests and responses
51+
private static final Map<String, Context> PSEUDO_HEADERS =
52+
Map.of(":authority", Context.REQUEST,
53+
":method", Context.REQUEST,
54+
":path", Context.REQUEST,
55+
":scheme", Context.REQUEST,
56+
":status", Context.RESPONSE);
57+
58+
// connection-specific, prohibited by RFC 9113 section 8.2.2
59+
private static final Set<String> PROHIBITED_HEADERS =
60+
Set.of("connection", "proxy-connection", "keep-alive",
61+
"transfer-encoding", "upgrade");
3862

3963
/** Used to check that if there are pseudo-headers, they go first */
4064
private boolean pseudoHeadersEnded;
@@ -60,11 +84,25 @@ public void onDecoded(CharSequence name, CharSequence value)
6084
if (n.startsWith(":")) {
6185
if (pseudoHeadersEnded) {
6286
throw newException("Unexpected pseudo-header '%s'", n);
63-
} else if (!PSEUDO_HEADERS.contains(n)) {
64-
throw newException("Unknown pseudo-header '%s'", n);
87+
} else {
88+
Context expectedContext = PSEUDO_HEADERS.get(n);
89+
if (expectedContext == null) {
90+
throw newException("Unknown pseudo-header '%s'", n);
91+
} else if (expectedContext != context) {
92+
throw newException("Pseudo-header '%s' is not valid in context " + context, n);
93+
}
6594
}
6695
} else {
6796
pseudoHeadersEnded = true;
97+
// Check for prohibited connection-specific headers.
98+
// Some servers echo request headers in push promises.
99+
// If the request was a HTTP/1.1 upgrade, it included some prohibited headers.
100+
// For compatibility, we ignore prohibited headers in push promises.
101+
if (context != Context.REQUEST) {
102+
if (PROHIBITED_HEADERS.contains(n)) {
103+
throw newException("Prohibited header name '%s'", n);
104+
}
105+
}
68106
// RFC-9113, section 8.2.1 for HTTP/2 and RFC-9114, section 4.2 state that
69107
// header name MUST be lowercase (and allowed characters)
70108
if (!Utils.isValidLowerCaseName(n)) {
@@ -84,6 +122,6 @@ protected String formatMessage(String message, String header) {
84122
protected UncheckedIOException newException(String message, String header)
85123
{
86124
return new UncheckedIOException(
87-
new IOException(formatMessage(message, header)));
125+
new ProtocolException(formatMessage(message, header)));
88126
}
89127
}

test/jdk/java/net/httpclient/http2/BadHeadersTest.java

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -23,7 +23,10 @@
2323

2424
/*
2525
* @test
26-
* @bug 8303965
26+
* @bug 8303965 8354276
27+
* @summary This test verifies the behaviour of the HttpClient when presented
28+
* with a HEADERS frame followed by CONTINUATION frames, and when presented
29+
* with bad header fields.
2730
* @library /test/lib /test/jdk/java/net/httpclient/lib
2831
* @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext
2932
* @run testng/othervm -Djdk.internal.httpclient.debug=true BadHeadersTest
@@ -44,6 +47,7 @@
4447
import java.io.IOException;
4548
import java.io.InputStream;
4649
import java.io.OutputStream;
50+
import java.net.ProtocolException;
4751
import java.net.URI;
4852
import java.net.http.HttpClient;
4953
import java.net.http.HttpHeaders;
@@ -76,6 +80,8 @@ public class BadHeadersTest {
7680
of(entry(":status", "200"), entry("hell o", "value")), // Space in the name
7781
of(entry(":status", "200"), entry("hello", "line1\r\n line2\r\n")), // Multiline value
7882
of(entry(":status", "200"), entry("hello", "DE" + ((char) 0x7F) + "L")), // Bad byte in value
83+
of(entry(":status", "200"), entry("connection", "close")), // Prohibited connection-specific header
84+
of(entry(":status", "200"), entry(":scheme", "https")), // Request pseudo-header in response
7985
of(entry("hello", "world!"), entry(":status", "200")) // Pseudo header is not the first one
8086
);
8187

@@ -86,7 +92,7 @@ public class BadHeadersTest {
8692
String https2URI;
8793

8894
/**
89-
* A function that returns a list of 1) a HEADERS frame ( with an empty
95+
* A function that returns a list of 1) one HEADERS frame ( with an empty
9096
* payload ), and 2) a CONTINUATION frame with the actual headers.
9197
*/
9298
static BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> oneContinuation =
@@ -100,7 +106,7 @@ public class BadHeadersTest {
100106
};
101107

102108
/**
103-
* A function that returns a list of a HEADERS frame followed by a number of
109+
* A function that returns a list of one HEADERS frame followed by a number of
104110
* CONTINUATION frames. Each frame contains just a single byte of payload.
105111
*/
106112
static BiFunction<Integer,List<ByteBuffer>,List<Http2Frame>> byteAtATime =
@@ -189,12 +195,13 @@ void testAsync(String uri,
189195
try {
190196
HttpResponse<String> response = cc.sendAsync(request, BodyHandlers.ofString()).get();
191197
fail("Expected exception, got :" + response + ", " + response.body());
192-
} catch (Throwable t0) {
198+
} catch (Exception t0) {
193199
System.out.println("Got EXPECTED: " + t0);
194200
if (t0 instanceof ExecutionException) {
195-
t0 = t0.getCause();
201+
t = t0.getCause();
202+
} else {
203+
t = t0;
196204
}
197-
t = t0;
198205
}
199206
assertDetailMessage(t, i);
200207
}
@@ -204,15 +211,21 @@ void testAsync(String uri,
204211
// sync with implementation.
205212
static void assertDetailMessage(Throwable throwable, int iterationIndex) {
206213
try {
207-
assertTrue(throwable instanceof IOException,
208-
"Expected IOException, got, " + throwable);
214+
assertTrue(throwable instanceof ProtocolException,
215+
"Expected ProtocolException, got " + throwable);
209216
assertTrue(throwable.getMessage().contains("malformed response"),
210217
"Expected \"malformed response\" in: " + throwable.getMessage());
211218

212219
if (iterationIndex == 0) { // unknown
213220
assertTrue(throwable.getMessage().contains("Unknown pseudo-header"),
214221
"Expected \"Unknown pseudo-header\" in: " + throwable.getMessage());
215-
} else if (iterationIndex == 4) { // unexpected
222+
} else if (iterationIndex == 4) { // prohibited
223+
assertTrue(throwable.getMessage().contains("Prohibited header name"),
224+
"Expected \"Prohibited header name\" in: " + throwable.getMessage());
225+
} else if (iterationIndex == 5) { // unexpected type
226+
assertTrue(throwable.getMessage().contains("not valid in context"),
227+
"Expected \"not valid in context\" in: " + throwable.getMessage());
228+
} else if (iterationIndex == 6) { // unexpected sequence
216229
assertTrue(throwable.getMessage().contains(" Unexpected pseudo-header"),
217230
"Expected \" Unexpected pseudo-header\" in: " + throwable.getMessage());
218231
} else {

0 commit comments

Comments
 (0)