Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/java.base/share/conf/net.properties
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,24 @@ ftp.nonProxyHosts=localhost|127.*|[::1]
#jdk.http.auth.proxying.disabledSchemes=
jdk.http.auth.tunneling.disabledSchemes=Basic

#
# Allow restricted HTTP request headers
#
# By default, the following request headers are not allowed to be set by user code
# in HttpRequests: "connection", "content-length", "expect", "host" and "upgrade".
# The 'jdk.httpclient.allowRestrictedHeaders' property allows one or more of these
# headers to be specified as a comma separated list to override the default restriction.
# The names are case-insensitive and white-space is ignored (removed before processing
# the list). Note, this capability is mostly intended for testing and isn't expected
# to be used in real deployments. Protocol errors or other undefined behavior is likely
# to occur when using them. The property is not set by default.
# Note also, that there may be other headers that are restricted from being set
# depending on the context. This includes the "Authorization" header when the
# relevant HttpClient has an authenticator set. These restrictions cannot be
# overridden by this property.
#
# jdk.httpclient.allowRestrictedHeaders=host
#
#
# Transparent NTLM HTTP authentication mode on Windows. Transparent authentication
# can be used for the NTLM scheme, where the security credentials based on the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,19 @@ private SecurityException checkPermissions() {
} catch (SecurityException e) {
return e;
}
String hostHeader = userHeaders.firstValue("Host").orElse(null);
if (hostHeader != null && !hostHeader.equalsIgnoreCase(u.getHost())) {
// user has set a Host header different to request URI
// must check that for URLPermission also
URI u1 = replaceHostInURI(u, hostHeader);
URLPermission p1 = permissionForServer(u1, method, userHeaders.map());
try {
assert acc != null;
sm.checkPermission(p1, acc);
} catch (SecurityException e) {
return e;
}
}
ProxySelector ps = client.proxySelector();
if (ps != null) {
if (!method.equals("CONNECT")) {
Expand All @@ -615,6 +628,15 @@ private SecurityException checkPermissions() {
return null;
}

private static URI replaceHostInURI(URI u, String hostPort) {
StringBuilder sb = new StringBuilder();
sb.append(u.getScheme())
.append("://")
.append(hostPort)
.append(u.getRawPath());
return URI.create(sb.toString());
}

HttpClient.Version version() {
return multi.version();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
package jdk.internal.net.http;

import java.io.IOException;
import java.net.http.HttpClient;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
Expand Down Expand Up @@ -65,6 +66,9 @@ final Exchange<T> getExchange() {
return exchange;
}

HttpClient client() {
return exchange.client();
}

/**
* Returns the {@link HttpConnection} instance to which this exchange is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.http.HttpClient;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandler;
import java.net.http.HttpResponse.BodySubscriber;
Expand Down Expand Up @@ -714,6 +715,10 @@ public void cancel() {
}
}

HttpClient client() {
return client;
}

String dbgString() {
return "Http1Exchange";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
Expand Down Expand Up @@ -59,7 +60,7 @@ class Http1Request {
private final Http1Exchange<?> http1Exchange;
private final HttpConnection connection;
private final HttpRequest.BodyPublisher requestPublisher;
private final HttpHeaders userHeaders;
private volatile HttpHeaders userHeaders;
private final HttpHeadersBuilder systemHeadersBuilder;
private volatile boolean streaming;
private volatile long contentLength;
Expand Down Expand Up @@ -91,14 +92,23 @@ private void logHeaders(String completeHeaders) {
}


private void collectHeaders0(StringBuilder sb) {
public void collectHeaders0(StringBuilder sb) {
BiPredicate<String,String> filter =
connection.headerFilter(request);

// Filter out 'Cookie:' headers, we will collect them at the end.
BiPredicate<String,String> nocookies = NOCOOKIES.and(filter);

HttpHeaders systemHeaders = systemHeadersBuilder.build();
HttpClient client = http1Exchange.client();

// Filter overridable headers from userHeaders
userHeaders = HttpHeaders.of(userHeaders.map(), Utils.CONTEXT_RESTRICTED(client));

final HttpHeaders uh = userHeaders;

// Filter any headers from systemHeaders that are set in userHeaders
systemHeaders = HttpHeaders.of(systemHeaders.map(), (k,v) -> uh.firstValue(k).isEmpty());

// If we're sending this request through a tunnel,
// then don't send any preemptive proxy-* headers that
Expand Down
12 changes: 12 additions & 0 deletions src/java.net.http/share/classes/jdk/internal/net/http/Stream.java
Original file line number Diff line number Diff line change
Expand Up @@ -617,8 +617,20 @@ private OutgoingHeaders<Stream<T>> headerFrame(long contentLength) {
if (contentLength > 0) {
h.setHeader("content-length", Long.toString(contentLength));
}
URI uri = request.uri();
if (uri != null) {
h.setHeader("host", Utils.hostString(request));
}
HttpHeaders sysh = filterHeaders(h.build());
HttpHeaders userh = filterHeaders(request.getUserHeaders());
// Filter context restricted from userHeaders
userh = HttpHeaders.of(userh.map(), Utils.CONTEXT_RESTRICTED(client()));

final HttpHeaders uh = userh;

// Filter any headers from systemHeaders that are set in userHeaders
sysh = HttpHeaders.of(sysh.map(), (k,v) -> uh.firstValue(k).isEmpty());

OutgoingHeaders<Stream<T>> f = new OutgoingHeaders<>(sysh, userh, this);
if (contentLength == 0) {
f.setFlag(HeadersFrame.END_STREAM);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLPermission;
import java.net.http.HttpClient;
import java.net.http.HttpHeaders;
import java.net.http.HttpTimeoutException;
import java.nio.ByteBuffer;
Expand Down Expand Up @@ -128,14 +129,23 @@ private static boolean hostnameVerificationDisabledValue() {

public static final BiPredicate<String,String> ACCEPT_ALL = (x,y) -> true;

private static final Set<String> DISALLOWED_HEADERS_SET;
private static final Set<String> DISALLOWED_HEADERS_SET = getDisallowedHeaders();

static {
// A case insensitive TreeSet of strings.
TreeSet<String> treeSet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
treeSet.addAll(Set.of("connection", "content-length",
"date", "expect", "from", "host", "upgrade", "via", "warning"));
DISALLOWED_HEADERS_SET = Collections.unmodifiableSet(treeSet);
private static Set<String> getDisallowedHeaders() {
Set<String> headers = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
headers.addAll(Set.of("connection", "content-length", "expect", "host", "upgrade"));

String v = getNetProperty("jdk.httpclient.allowRestrictedHeaders");
if (v != null) {
// any headers found are removed from set.
String[] tokens = v.trim().split(",");
for (String token : tokens) {
headers.remove(token);
}
return Collections.unmodifiableSet(headers);
} else {
return Collections.unmodifiableSet(headers);
}
}

public static final BiPredicate<String, String>
Expand All @@ -158,6 +168,20 @@ private static boolean hostnameVerificationDisabledValue() {
};

private static final Predicate<String> IS_HOST = "host"::equalsIgnoreCase;

// Headers that are not generally restricted, and can therefore be set by users,
// but can in some contexts be overridden by the implementation.
// Currently, only contains "Authorization" which will
// be overridden, when an Authenticator is set on the HttpClient.
// Needs to be BiPred<String,String> to fit with general form of predicates
// used by caller.

public static final BiPredicate<String, String> CONTEXT_RESTRICTED(HttpClient client) {
return (k, v) -> client.authenticator() == null ||
! (k.equalsIgnoreCase("Authorization")
&& k.equalsIgnoreCase("Proxy-Authorization"));
}

private static final Predicate<String> IS_PROXY_HEADER = (k) ->
k != null && k.length() > 6 && "proxy-".equalsIgnoreCase(k.substring(0,6));
private static final Predicate<String> NO_PROXY_HEADER =
Expand Down Expand Up @@ -335,8 +359,8 @@ public static URLPermission permissionForServer(URI uri,
Stream<String> headers) {
String urlString = new StringBuilder()
.append(uri.getScheme()).append("://")
.append(uri.getAuthority())
.append(uri.getPath()).toString();
.append(uri.getRawAuthority())
.append(uri.getRawPath()).toString();

StringBuilder actionStringBuilder = new StringBuilder(method);
String collected = headers.collect(joining(","));
Expand Down Expand Up @@ -800,6 +824,33 @@ public static Logger getDebugLogger(Supplier<String> dbgTag, boolean on) {
return getDebugLogger(dbgTag, errLevel);
}

/**
* Return the host string from a HttpRequestImpl
*
* @param request
* @return
*/
public static String hostString(HttpRequestImpl request) {
URI uri = request.uri();
int port = uri.getPort();
String host = uri.getHost();

boolean defaultPort;
if (port == -1) {
defaultPort = true;
} else if (uri.getScheme().equalsIgnoreCase("https")) {
defaultPort = port == 443;
} else {
defaultPort = port == 80;
}

if (defaultPort) {
return host;
} else {
return host + ":" + Integer.toString(port);
}
}

/**
* Get a logger for debug HPACK traces.The logger should only be used
* with levels whose severity is {@code <= DEBUG}.
Expand Down
13 changes: 4 additions & 9 deletions test/jdk/java/net/httpclient/RequestBuilderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ public void testHeaders() {

// headers that are allowed now, but weren't before
private static final Set<String> FORMERLY_RESTRICTED = Set.of("referer", "origin",
"OriGin", "Referer");
"OriGin", "Referer", "Date", "via", "WarnIng");

@Test
public void testFormerlyRestricted() throws URISyntaxException {
Expand All @@ -354,14 +354,9 @@ public void testFormerlyRestricted() throws URISyntaxException {
}

private static final Set<String> RESTRICTED = Set.of("connection", "content-length",
"date", "expect", "from", "host",
"upgrade", "via", "warning",
"Connection", "Content-Length",
"DATE", "eXpect", "frOm", "hosT",
"upgradE", "vIa", "Warning",
"CONNection", "CONTENT-LENGTH",
"Date", "EXPECT", "From", "Host",
"Upgrade", "Via", "WARNING");
"expect", "host", "upgrade", "Connection", "Content-Length",
"eXpect", "hosT", "upgradE", "CONNection", "CONTENT-LENGTH",
"EXPECT", "Host", "Upgrade");

interface WithHeader {
HttpRequest.Builder withHeader(HttpRequest.Builder builder, String name, String value);
Expand Down
97 changes: 97 additions & 0 deletions test/jdk/java/net/httpclient/RestrictedHeadersTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright (c) 2018, 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.
*/

/*
* @test
* @bug 8178699
* @modules java.net.http
* @run main/othervm RestrictedHeadersTest
* @run main/othervm -Djdk.httpclient.allowRestrictedHeaders=content-length,connection RestrictedHeadersTest content-length connection
* @run main/othervm -Djdk.httpclient.allowRestrictedHeaders=host,upgrade RestrictedHeadersTest host upgrade
* @run main/othervm -Djdk.httpclient.allowRestrictedHeaders=via RestrictedHeadersTest via
*/

import java.net.URI;
import java.net.http.HttpRequest;
import java.util.Set;

public class RestrictedHeadersTest {
public static void main(String[] args) {
if (args.length == 0) {
runDefaultTest();
} else {
runTest(Set.of(args));
}
}

// This list must be same as impl

static Set<String> defaultRestrictedHeaders =
Set.of("connection", "content-length", "expect", "host", "upgrade");

private static void runDefaultTest() {
System.out.println("DEFAULT TEST: no property set");
for (String header : defaultRestrictedHeaders) {
checkHeader(header, "foo", false);
}
// miscellaneous others that should succeed
checkHeader("foobar", "barfoo", true);
checkHeader("date", "today", true);
}

private static void checkHeader(String name, String value, boolean succeed) {
try {
HttpRequest request = HttpRequest.newBuilder(URI.create("https://foo.com/"))
.header(name, value)
.GET()
.build();
if (!succeed) {
String s = name+"/"+value+" should have failed";
throw new RuntimeException(s);
}
System.out.printf("%s = %s succeeded as expected\n", name, value);
} catch (IllegalArgumentException iae) {
if (succeed) {
String s = name+"/"+value+" should have succeeded";
throw new RuntimeException(s);
}
System.out.printf("%s = %s failed as expected\n", name, value);
}
}

// args is the Set of allowed restricted headers
private static void runTest(Set<String> args) {
System.out.print("RUNTEST: allowed headers set in property: ");
for (String arg : args) System.out.printf("%s ", arg);
System.out.println("");

for (String header : args) {
checkHeader(header, "val", true);
}
for (String header : defaultRestrictedHeaders) {
if (!args.contains(header)) {
checkHeader(header, "foo", false);
}
}
}
}
Loading