undefect. CWE-407 — 63 sites patched across 27 ecosystems
Authors: russell@unturf.com · brackishbert@gmail.com · foxhop.net · TimeHexOn.com Patches, unit tests, benchmarks, whitepaper, and outreach briefs. Public domain — no copyright claimed. Use freely.
This commit is contained in:
commit
0a580b313d
70422 changed files with 17213626 additions and 0 deletions
473
test/jdk/java/net/httpclient/http3/H3MalformedResponseTest.java
Normal file
473
test/jdk/java/net/httpclient/http3/H3MalformedResponseTest.java
Normal file
|
|
@ -0,0 +1,473 @@
|
|||
/*
|
||||
* Copyright (c) 2024, 2026, Oracle and/or its affiliates. All rights reserved.
|
||||
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||
*
|
||||
* This code is free software; you can redistribute it and/or modify it
|
||||
* under the terms of the GNU General Public License version 2 only, as
|
||||
* published by the Free Software Foundation.
|
||||
*
|
||||
* This code is distributed in the hope that it will be useful, but WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* version 2 for more details (a copy is included in the LICENSE file that
|
||||
* accompanied this code).
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License version
|
||||
* 2 along with this work; if not, write to the Free Software Foundation,
|
||||
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
*
|
||||
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
|
||||
* or visit www.oracle.com if you need additional information or have any
|
||||
* questions.
|
||||
*/
|
||||
|
||||
import jdk.httpclient.test.lib.common.HttpServerAdapters;
|
||||
import jdk.httpclient.test.lib.quic.QuicStandaloneServer;
|
||||
import jdk.internal.net.http.common.Logger;
|
||||
import jdk.internal.net.http.common.Utils;
|
||||
import jdk.internal.net.quic.QuicVersion;
|
||||
import jdk.test.lib.net.SimpleSSLContext;
|
||||
import jdk.test.lib.net.URIBuilder;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpClient.Version;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.net.http.HttpResponse.BodyHandlers;
|
||||
import java.util.HexFormat;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
import static java.net.http.HttpClient.Builder.NO_PROXY;
|
||||
import static java.net.http.HttpOption.H3_DISCOVERY;
|
||||
import static java.net.http.HttpOption.Http3DiscoveryMode.HTTP_3_URI_ONLY;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
/*
|
||||
* @test
|
||||
* @bug 8369595
|
||||
* @summary Verifies that the HTTP/3 malformed responses are correctly handled
|
||||
* @library /test/jdk/java/net/httpclient/lib
|
||||
* /test/lib
|
||||
* @run junit ${test.main.class}
|
||||
*/
|
||||
|
||||
/// Verifies that the HTTP/3 malformed responses are correctly handled.
|
||||
///
|
||||
/// ### HTTP/3 `HEADERS` frame & QPACK Field Section encoding crash course
|
||||
///
|
||||
/// Consider an HTTP/3 `HEADERS` frame that carries:
|
||||
///
|
||||
/// ```
|
||||
/// :status: 200
|
||||
/// content-length: 2
|
||||
/// ```
|
||||
///
|
||||
/// This will be encoded as the following byte sequence:
|
||||
///
|
||||
/// ```
|
||||
/// 01 06 00 00 D9 54 01 32
|
||||
/// ```
|
||||
///
|
||||
/// Let's start with decoding the HTTP/3 frame:
|
||||
///
|
||||
/// - `01`: Frame Type (`01` denotes `HEADERS``)
|
||||
///
|
||||
/// - `06`: Payload length (6 bytes)
|
||||
///
|
||||
/// Figured this is a `HEADERS` frame containing 6 bytes: `00 00 D9 54 01 32`.
|
||||
/// Let's decode the QPACK Field Section
|
||||
///
|
||||
/// - `00`: Required Insert Count (0)
|
||||
///
|
||||
/// - `00`: Base (0)
|
||||
///
|
||||
/// - `D9`:
|
||||
/// QPACK has a static table (indexed from 0) and `:status: 200` is at the
|
||||
/// static-table index 25.
|
||||
///
|
||||
/// ```
|
||||
/// D9 = <1101 1001>
|
||||
/// = <1> (Indexed Field Line)
|
||||
/// + <1> (Static Table)
|
||||
/// + <01 1001> (entry index = 25)
|
||||
/// ```
|
||||
///
|
||||
/// - `54 01 32`:
|
||||
/// `content-length: 2` can be encoded as a *literal field line with name
|
||||
/// reference* using the static name `content-length` (static index 4) and
|
||||
/// the literal value `2`.
|
||||
///
|
||||
/// ```
|
||||
/// 54 01 32 = <0101 0100 0000 0001 0011 0010>
|
||||
/// = <0> (Literal Field Line)
|
||||
/// + <1> (Name Reference)
|
||||
/// + <0100> (entry index = 4)
|
||||
/// + <0000 0001> (value length = 1 byte)
|
||||
/// + <0011 0010> (value = ASCII "2" = 0x32 = 50)
|
||||
/// ```
|
||||
///
|
||||
/// Note that the `value length` field (i.e., `0000 0001`) follows a variable
|
||||
/// coding scheme:
|
||||
///
|
||||
/// | Prefix | Total size | Payload size | Max value |
|
||||
/// | --------------------- | ---------- | ------------ | ------------- |
|
||||
/// | `00xx xxxx` | 1 byte | 6 bits | 63 |
|
||||
/// | `01xx xxxx xxxx xxxx` | 2 bytes | 14 bits | 16,383 |
|
||||
/// | `10xx xxx…` | 4 bytes | 30 bits | 1,073,741,823 |
|
||||
/// | `11xx xxx…` | 8 bytes | 62 bits | 4.61e18 |
|
||||
class H3MalformedResponseTest {
|
||||
|
||||
private static final String CLASS_NAME = H3MalformedResponseTest.class.getSimpleName();
|
||||
|
||||
private static final Logger LOGGER = Utils.getDebugLogger(CLASS_NAME::toString, Utils.DEBUG);
|
||||
|
||||
private static final SSLContext SSL_CONTEXT = SimpleSSLContext.findSSLContext();
|
||||
|
||||
private static QuicStandaloneServer SERVER;
|
||||
|
||||
private static HttpRequest REQUEST;
|
||||
|
||||
@BeforeAll
|
||||
static void setUp() throws Exception {
|
||||
|
||||
// Create and start the server
|
||||
SERVER = QuicStandaloneServer.newBuilder()
|
||||
.availableVersions(new QuicVersion[]{QuicVersion.QUIC_V1})
|
||||
.sslContext(SSL_CONTEXT)
|
||||
.alpn("h3")
|
||||
.build();
|
||||
SERVER.start();
|
||||
LOGGER.log("Server is started at {}", SERVER.getAddress());
|
||||
|
||||
// Create the request
|
||||
var requestURI = URIBuilder.newBuilder()
|
||||
.scheme("https")
|
||||
.loopback()
|
||||
.port(SERVER.getAddress().getPort())
|
||||
.path("/" + CLASS_NAME)
|
||||
.build();
|
||||
REQUEST = HttpRequest.newBuilder(requestURI)
|
||||
.version(Version.HTTP_3)
|
||||
.setOption(H3_DISCOVERY, HTTP_3_URI_ONLY)
|
||||
.build();
|
||||
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void tearDown() {
|
||||
close("server", SERVER);
|
||||
}
|
||||
|
||||
private static void close(String name, AutoCloseable closeable) {
|
||||
if (closeable != null) {
|
||||
LOGGER.log("Closing {}", name);
|
||||
try {
|
||||
closeable.close();
|
||||
} catch (Exception e) {
|
||||
LOGGER.log("Could not close " + name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Malformed responses that should not be accepted by the client, but
|
||||
/// should neither cause the connection to get closed.
|
||||
static Object[][] malformedResponsesPreservingConnection() {
|
||||
return new Object[][]{
|
||||
{"empty", IOException.class, parseHex("")},
|
||||
{"non-final response", IOException.class, parseHex(
|
||||
"01040000", // headers, length 4, section prefix
|
||||
"ff00" // :status:100
|
||||
)},
|
||||
{"uppercase header name", ProtocolException.class, parseHex(
|
||||
"01090000", // headers, length 9, section prefix
|
||||
"d9", // :status:200
|
||||
"234147450130", // AGE:0
|
||||
"000100" // data, 1 byte
|
||||
)},
|
||||
{"content too long", IOException.class, parseHex(
|
||||
"01040000", // headers, length 4, section prefix
|
||||
"d9", // :status:200
|
||||
"c4", // content-length:0
|
||||
"000100" // data, 1 byte
|
||||
)},
|
||||
{"content too short", IOException.class, parseHex(
|
||||
"01060000", // headers, length 6, section prefix
|
||||
"d9", // :status:200
|
||||
"540132", // content-length:2
|
||||
"000100" // data, 1 byte
|
||||
)},
|
||||
{"text in content-length", ProtocolException.class, parseHex(
|
||||
"01060000" + // headers, length 6, section prefix
|
||||
"d9" + // :status:200
|
||||
"540161" + // content-length:a
|
||||
"000100" // data, 1 byte
|
||||
)},
|
||||
{"connection: close", ProtocolException.class, parseHex(
|
||||
"01150000", // headers, length 21, section prefix
|
||||
"d9", // :status:200
|
||||
"2703636F6E6E656374696F6E05636C6F7365" + // connection:close
|
||||
"000100" // data, 1 byte
|
||||
)},
|
||||
// request pseudo-headers in response
|
||||
{":method in response", ProtocolException.class, parseHex(
|
||||
"01040000", // headers, length 4, section prefix
|
||||
"d9", // :status:200
|
||||
"d1", // :method:get
|
||||
"000100" // data, 1 byte
|
||||
)},
|
||||
{":authority in response", ProtocolException.class, parseHex(
|
||||
"01100000", // headers, length 16, section prefix
|
||||
"d9", // :status:200
|
||||
"508b089d5c0b8170dc702fbce7", // :authority
|
||||
"000100" // data, 1 byte
|
||||
)},
|
||||
{":path in response", ProtocolException.class, parseHex(
|
||||
"010a0000", // headers, length 10, section prefix
|
||||
"d9", // :status:200
|
||||
"51856272d141ff", // :path
|
||||
"000100" // data, 1 byte
|
||||
)},
|
||||
{":scheme in response", ProtocolException.class, parseHex(
|
||||
"01040000", // headers, length 4, section prefix
|
||||
"d9", // :status:200
|
||||
"d7", // :scheme:https
|
||||
"000100" // data, 1 byte
|
||||
)},
|
||||
{"undefined pseudo-header", ProtocolException.class, parseHex(
|
||||
"01080000", // headers, length 8, section prefix
|
||||
"d9", // :status:200
|
||||
"223A6D0130", // :m:0
|
||||
"000100" // data, 1 byte
|
||||
)},
|
||||
{"pseudo-header after regular", ProtocolException.class, parseHex(
|
||||
"011a0000", // headers, length 26, section prefix
|
||||
"5f5094ca3ee35a74a6b589418b5258132b1aa496ca8747", //user-agent
|
||||
"d9", // :status:200
|
||||
"000100" // data, 1 byte
|
||||
)},
|
||||
{"trailer", IOException.class, parseHex(
|
||||
"01020000" // headers, length 2, section prefix
|
||||
)},
|
||||
{"trailer+data", IOException.class, parseHex(
|
||||
"01020000", // headers, length 2, section prefix
|
||||
"000100" // data, 1 byte
|
||||
)},
|
||||
// valid characters include \t, 0x20-0x7e, 0x80-0xff (RFC 9110, section 5.5)
|
||||
{"invalid character in field value 00", ProtocolException.class, parseHex(
|
||||
"01060000", // headers, length 6, section prefix
|
||||
"d9", // :status:200
|
||||
"570100", // etag:\0
|
||||
"000100" // data, 1 byte
|
||||
)},
|
||||
{"invalid character in field value 0a", ProtocolException.class, parseHex(
|
||||
"01060000", // headers, length 6, section prefix
|
||||
"d9", // :status:200
|
||||
"57010a", // etag:\n
|
||||
"000100" // data, 1 byte
|
||||
)},
|
||||
{"invalid character in field value 0d", ProtocolException.class, parseHex(
|
||||
"01060000", // headers, length 6, section prefix
|
||||
"d9", // :status:200
|
||||
"57010d", // etag:\r
|
||||
"000100" // data, 1 byte
|
||||
)},
|
||||
{"invalid character in field value 7f", ProtocolException.class, parseHex(
|
||||
"01060000", // headers, length 6, section prefix
|
||||
"d9", // :status:200
|
||||
"57017f", // etag: 0x7f
|
||||
"000100" // data, 1 byte
|
||||
)},
|
||||
};
|
||||
}
|
||||
|
||||
/// Malformed responses that should not be accepted by the client.
|
||||
/// They might or might not cause the connection to get closed (`H3_FRAME_UNEXPECTED`).
|
||||
static Object[][] malformedResponses() {
|
||||
// data before headers is covered by H3ErrorHandlingTest
|
||||
return new Object[][]{
|
||||
{"100+data", IOException.class, parseHex(
|
||||
"01040000", // headers, length 4, section prefix
|
||||
"ff00", // :status:100
|
||||
"000100" // data, 1 byte
|
||||
)},
|
||||
{"100+data+200", IOException.class, parseHex(
|
||||
"01040000", // headers, length 4, section prefix
|
||||
"ff00", // :status:100
|
||||
"000100", // data, 1 byte
|
||||
"01030000", // headers, length 3, section prefix
|
||||
"d9" // :status:200
|
||||
)},
|
||||
{"200+data+200", IOException.class, parseHex(
|
||||
"01030000", // headers, length 3, section prefix
|
||||
"d9", // :status:200
|
||||
"000100", // data, 1 byte
|
||||
"01030000", // headers, length 3, section prefix
|
||||
"d9" // :status:200
|
||||
)},
|
||||
{"200+data+100", IOException.class, parseHex(
|
||||
"01030000", // headers, length 3, section prefix
|
||||
"d9", // :status:200
|
||||
"000100", // data, 1 byte
|
||||
"01040000", // headers, length 4, section prefix
|
||||
"ff00" // :status:100
|
||||
)},
|
||||
{"200+data+trailers+data", ProtocolException.class, parseHex(
|
||||
"01030000", // headers, length 3, section prefix
|
||||
"d9", // :status:200
|
||||
"000100", // data, 1 byte
|
||||
"01020000", // trailers, length 2, section prefix
|
||||
"000100" // data, 1 byte
|
||||
)},
|
||||
{"200+trailers+data", IOException.class, parseHex(
|
||||
"01030000", // headers, length 3, section prefix
|
||||
"d9", // :status:200
|
||||
"01020000", // trailers, length 2, section prefix
|
||||
"000100" // data, 1 byte
|
||||
)},
|
||||
{"200+200", IOException.class, parseHex(
|
||||
"01030000", // headers, length 3, section prefix
|
||||
"d9", // :status:200
|
||||
"01030000", // headers, length 3, section prefix
|
||||
"d9" // :status:200
|
||||
)},
|
||||
{"200+100", IOException.class, parseHex(
|
||||
"01030000", // headers, length 3, section prefix
|
||||
"d9", // :status:200
|
||||
"01040000", // headers, length 4, section prefix
|
||||
"ff00" // :status:100
|
||||
)},
|
||||
};
|
||||
}
|
||||
|
||||
/// Well-formed responses that should be accepted by the client.
|
||||
static Object[][] wellFormedResponses() {
|
||||
return new Object[][]{
|
||||
{"100+200+data+reserved", parseHex(
|
||||
"01040000", // headers, length 4, section prefix
|
||||
"ff00", // :status:100
|
||||
"01030000", // headers, length 3, section prefix
|
||||
"d9", // :status:200
|
||||
"000100", // data, 1 byte
|
||||
"210100" // reserved, 1 byte
|
||||
)},
|
||||
{"200+data+reserved", parseHex(
|
||||
"01030000", // headers, length 3, section prefix
|
||||
"d9", // :status:200
|
||||
"000100", // data, 1 byte
|
||||
"210100" // reserved, 1 byte
|
||||
)},
|
||||
{"200+data", parseHex(
|
||||
"01030000", // headers, length 3, section prefix
|
||||
"d9", // :status:200
|
||||
"000100" // data, 1 byte
|
||||
)},
|
||||
{"200+user-agent+data", parseHex(
|
||||
"011a0000", // headers, length 26, section prefix
|
||||
"d9", // :status:200
|
||||
"5f5094ca3ee35a74a6b589418b5258132b1aa496ca8747", //user-agent
|
||||
"000100" // data, 1 byte
|
||||
)},
|
||||
{"200", parseHex(
|
||||
"01030000", // headers, length 3, section prefix
|
||||
"d9" // :status:200
|
||||
)},
|
||||
{"200+data+data", parseHex(
|
||||
"01030000", // headers, length 3, section prefix
|
||||
"d9", // :status:200
|
||||
"000100", // data, 1 byte
|
||||
"000100" // data, 1 byte
|
||||
)},
|
||||
{"200+data+trailers", parseHex(
|
||||
"01030000", // headers, length 3, section prefix
|
||||
"d9", // :status:200
|
||||
"000100", // data, 1 byte
|
||||
"01020000" // trailers, length 2, section prefix
|
||||
)},
|
||||
{"200+trailers", parseHex(
|
||||
"01030000", // headers, length 3, section prefix
|
||||
"d9", // :status:200
|
||||
"01020000" // trailers, length 2, section prefix
|
||||
)},
|
||||
};
|
||||
}
|
||||
|
||||
private static byte[] parseHex(String... strings) {
|
||||
var buffer = new StringBuilder();
|
||||
for (String string : strings) {
|
||||
buffer.append(string);
|
||||
}
|
||||
return HexFormat.of().parseHex(buffer.toString());
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("wellFormedResponses")
|
||||
void testWellFormedResponse(String desc, byte[] serverResponseBytes) throws Exception {
|
||||
var connectionTerminated = configureServerResponse(serverResponseBytes);
|
||||
try (var client = createClient()) {
|
||||
final HttpResponse<Void> response = client.send(REQUEST, BodyHandlers.discarding());
|
||||
assertEquals(200, response.statusCode());
|
||||
assertFalse(connectionTerminated.getAsBoolean(), "Expected the connection to be open");
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("malformedResponsesPreservingConnection")
|
||||
void testMalformedResponsePreservingConnection(
|
||||
String desc,
|
||||
Class<? extends Exception> exceptionClass,
|
||||
byte[] serverResponseBytes) {
|
||||
var connectionTerminated = configureServerResponse(serverResponseBytes);
|
||||
try (var client = createClient()) {
|
||||
var exception = assertThrows(exceptionClass, () -> client.send(REQUEST, BodyHandlers.discarding()));
|
||||
LOGGER.log("Got expected exception for: " + desc, exception);
|
||||
assertFalse(connectionTerminated.getAsBoolean(), "Expected the connection to be open");
|
||||
}
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("malformedResponses")
|
||||
void testMalformedResponse(
|
||||
String desc,
|
||||
Class<? extends Exception> exceptionClass,
|
||||
byte[] serverResponseBytes) {
|
||||
configureServerResponse(serverResponseBytes);
|
||||
try (var client = createClient()) {
|
||||
var exception = assertThrows(exceptionClass, () -> client.send(REQUEST, BodyHandlers.discarding()));
|
||||
LOGGER.log("Got expected exception for: " + desc, exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static HttpClient createClient() {
|
||||
return HttpServerAdapters.createClientBuilderForH3()
|
||||
.proxy(NO_PROXY)
|
||||
.version(Version.HTTP_3)
|
||||
.sslContext(SSL_CONTEXT)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static BooleanSupplier configureServerResponse(byte[] serverResponseBytes) {
|
||||
var connectionTerminated = new AtomicBoolean();
|
||||
SERVER.setHandler((c, s)-> {
|
||||
try (OutputStream outputStream = s.outputStream()) {
|
||||
outputStream.write(serverResponseBytes);
|
||||
}
|
||||
c.futureTerminationCause().handle((_, _) -> {
|
||||
connectionTerminated.set(true);
|
||||
return true;
|
||||
});
|
||||
});
|
||||
return connectionTerminated::get;
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue