Jetty 10 - Configurable Unsafe Host Header (#9283)

* Adding HttpCompliance.DUPLICATE_HOST_HEADERS
  + Optional compliance that allowance duplicate host headers.
* Adding HttpCompliance.UNSAFE_HOST_HEADER
  + Optional compliance that allows unsafe host headers.
* Adding warning logging for bad Host / authority situations

Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com>
This commit is contained in:
Joakim Erdfelt 2023-02-03 08:30:07 -06:00 committed by GitHub
parent f0cba0807a
commit 016de2faeb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 427 additions and 121 deletions

View File

@ -103,7 +103,21 @@ public final class HttpCompliance implements ComplianceViolation.Mode
* line of a single token with neither a colon nor value following, to be interpreted as a field name with no value. * line of a single token with neither a colon nor value following, to be interpreted as a field name with no value.
* A deployment may include this violation to allow such fields to be in a received request. * A deployment may include this violation to allow such fields to be in a received request.
*/ */
NO_COLON_AFTER_FIELD_NAME("https://tools.ietf.org/html/rfc7230#section-3.2", "Fields must have a Colon"); NO_COLON_AFTER_FIELD_NAME("https://tools.ietf.org/html/rfc7230#section-3.2", "Fields must have a Colon"),
/**
* Since <a href="https://www.rfc-editor.org/rfc/rfc7230#section-5.4">RFC 7230: Section 5.4</a>, the HTTP protocol
* says that a Server must reject a request duplicate host headers.
* A deployment may include this violation to allow duplicate host headers on a received request.
*/
DUPLICATE_HOST_HEADERS("https://www.rfc-editor.org/rfc/rfc7230#section-5.4", "Duplicate Host Header"),
/**
* Since <a href="https://www.rfc-editor.org/rfc/rfc7230#section-2.7.1">RFC 7230</a>, the HTTP protocol
* should reject a request if the Host headers contains an invalid / unsafe authority.
* A deployment may include this violation to allow unsafe host headesr on a received request.
*/
UNSAFE_HOST_HEADER("https://www.rfc-editor.org/rfc/rfc7230#section-2.7.1", "Invalid Authority");
private final String url; private final String url;
private final String description; private final String description;

View File

@ -24,6 +24,7 @@ import java.util.Map;
import org.eclipse.jetty.http.HttpTokens.EndOfContent; import org.eclipse.jetty.http.HttpTokens.EndOfContent;
import org.eclipse.jetty.util.BufferUtil; import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.Index; import org.eclipse.jetty.util.Index;
import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.Utf8StringBuilder; import org.eclipse.jetty.util.Utf8StringBuilder;
@ -33,10 +34,12 @@ import org.slf4j.LoggerFactory;
import static org.eclipse.jetty.http.HttpCompliance.RFC7230; import static org.eclipse.jetty.http.HttpCompliance.RFC7230;
import static org.eclipse.jetty.http.HttpCompliance.Violation; import static org.eclipse.jetty.http.HttpCompliance.Violation;
import static org.eclipse.jetty.http.HttpCompliance.Violation.CASE_SENSITIVE_FIELD_NAME; import static org.eclipse.jetty.http.HttpCompliance.Violation.CASE_SENSITIVE_FIELD_NAME;
import static org.eclipse.jetty.http.HttpCompliance.Violation.DUPLICATE_HOST_HEADERS;
import static org.eclipse.jetty.http.HttpCompliance.Violation.HTTP_0_9; import static org.eclipse.jetty.http.HttpCompliance.Violation.HTTP_0_9;
import static org.eclipse.jetty.http.HttpCompliance.Violation.MULTIPLE_CONTENT_LENGTHS; import static org.eclipse.jetty.http.HttpCompliance.Violation.MULTIPLE_CONTENT_LENGTHS;
import static org.eclipse.jetty.http.HttpCompliance.Violation.NO_COLON_AFTER_FIELD_NAME; import static org.eclipse.jetty.http.HttpCompliance.Violation.NO_COLON_AFTER_FIELD_NAME;
import static org.eclipse.jetty.http.HttpCompliance.Violation.TRANSFER_ENCODING_WITH_CONTENT_LENGTH; import static org.eclipse.jetty.http.HttpCompliance.Violation.TRANSFER_ENCODING_WITH_CONTENT_LENGTH;
import static org.eclipse.jetty.http.HttpCompliance.Violation.UNSAFE_HOST_HEADER;
import static org.eclipse.jetty.http.HttpCompliance.Violation.WHITESPACE_AFTER_FIELD_NAME; import static org.eclipse.jetty.http.HttpCompliance.Violation.WHITESPACE_AFTER_FIELD_NAME;
/** /**
@ -226,7 +229,7 @@ public class HttpParser
private String _valueString; private String _valueString;
private int _responseStatus; private int _responseStatus;
private int _headerBytes; private int _headerBytes;
private boolean _host; private String _parsedHost;
private boolean _headerComplete; private boolean _headerComplete;
private volatile State _state = State.START; private volatile State _state = State.START;
private volatile FieldState _fieldState = FieldState.FIELD; private volatile FieldState _fieldState = FieldState.FIELD;
@ -1028,14 +1031,28 @@ public class HttpParser
break; break;
case HOST: case HOST:
if (_host) if (_parsedHost != null)
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad Host: multiple headers"); {
_host = true; if (LOG.isWarnEnabled())
LOG.warn("Encountered multiple `Host` headers. Previous `Host` header already seen as `{}`, new `Host` header has appeared as `{}`", _parsedHost, _valueString);
checkViolation(DUPLICATE_HOST_HEADERS);
}
_parsedHost = _valueString;
if (!(_field instanceof HostPortHttpField) && _valueString != null && !_valueString.isEmpty()) if (!(_field instanceof HostPortHttpField) && _valueString != null && !_valueString.isEmpty())
{ {
_field = new HostPortHttpField(_header, HostPort hostPort;
CASE_SENSITIVE_FIELD_NAME.isAllowedBy(_complianceMode) ? _headerString : _header.asString(), if (UNSAFE_HOST_HEADER.isAllowedBy(_complianceMode))
_valueString); {
_field = new HostPortHttpField(_header,
CASE_SENSITIVE_FIELD_NAME.isAllowedBy(_complianceMode) ? _headerString : _header.asString(),
HostPort.unsafe(_valueString));
}
else
{
_field = new HostPortHttpField(_header,
CASE_SENSITIVE_FIELD_NAME.isAllowedBy(_complianceMode) ? _headerString : _header.asString(),
_valueString);
}
addToFieldCache = _fieldCache.isEnabled(); addToFieldCache = _fieldCache.isEnabled();
} }
break; break;
@ -1072,6 +1089,8 @@ public class HttpParser
_fieldCache.add(_field); _fieldCache.add(_field);
} }
} }
if (LOG.isDebugEnabled())
LOG.debug("parsedHeader({}) header={}, headerString=[{}], valueString=[{}]", _field, _header, _headerString, _valueString);
_handler.parsedHeader(_field != null ? _field : new HttpField(_header, _headerString, _valueString)); _handler.parsedHeader(_field != null ? _field : new HttpField(_header, _headerString, _valueString));
} }
@ -1183,7 +1202,7 @@ public class HttpParser
} }
// Was there a required host header? // Was there a required host header?
if (!_host && _version == HttpVersion.HTTP_1_1 && _requestHandler != null) if (_parsedHost == null && _version == HttpVersion.HTTP_1_1 && _requestHandler != null)
{ {
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "No Host"); throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "No Host");
} }
@ -1888,7 +1907,7 @@ public class HttpParser
_responseStatus = 0; _responseStatus = 0;
_contentChunk = null; _contentChunk = null;
_headerBytes = 0; _headerBytes = 0;
_host = false; _parsedHost = null;
_headerComplete = false; _headerComplete = false;
} }

View File

@ -18,6 +18,7 @@ import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.stream.Stream;
import org.eclipse.jetty.http.HttpParser.State; import org.eclipse.jetty.http.HttpParser.State;
import org.eclipse.jetty.logging.StacklessLogging; import org.eclipse.jetty.logging.StacklessLogging;
@ -28,6 +29,8 @@ import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.ValueSource;
import static org.eclipse.jetty.http.HttpCompliance.Violation.CASE_INSENSITIVE_METHOD; import static org.eclipse.jetty.http.HttpCompliance.Violation.CASE_INSENSITIVE_METHOD;
@ -39,6 +42,7 @@ import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
@ -2041,17 +2045,95 @@ public class HttpParserTest
assertEquals(8888, _port); assertEquals(8888, _port);
} }
public static Stream<String> badHostHeaderSource()
{
return List.of(
":80", // no host, port only
"host:", // no port
"127.0.0.1:", // no port
"[0::0::0::0::1", // no IP literal ending bracket
"0::0::0::0::1]", // no IP literal starting bracket
"[0::0::0::0::1]:", // no port
"[0::0::0::1]", // not valid to Java (InetAddress, InetSocketAddress, or URI) : "Expected hex digits or IPv4 address"
"[0::0::0::1]:80", // not valid to Java (InetAddress, InetSocketAddress, or URI) : "Expected hex digits or IPv4 address"
"0:1:2:3:4:5:6", // not valid to Java (InetAddress, InetSocketAddress, or URI) : "IPv6 address too short"
"host:xxx", // invalid port
"127.0.0.1:xxx", // host + invalid port
"[0::0::0::0::1]:xxx", // ipv6 + invalid port
"host:-80", // host + invalid port
"127.0.0.1:-80", // ipv4 + invalid port
"[0::0::0::0::1]:-80", // ipv6 + invalid port
"127.0.0.1:65536", // ipv4 + port value too high
"a b c d", // whitespace in reg-name
"a\to\tz", // tabs in reg-name
"hosta, hostb, hostc", // space sin reg-name
"[ab:cd:ef:gh:ij:kl:mn]", // invalid ipv6 address
// Examples of bad Host header values (usually client bugs that shouldn't allow them)
"Group - Machine", // spaces
"<calculated when request is sent>",
"[link](https://example.org/)",
"example.org/zed", // has slash
// common hacking attempts, seen as values on the `Host:` request header
"| ping 127.0.0.1 -n 10",
"%uf%80%ff%xx%uffff",
"[${jndi${:-:}ldap${:-:}]", // log4j hacking
"[${jndi:ldap://example.org:59377/nessus}]", // log4j hacking
"${ip}", // variation of log4j hack
"' *; host xyz.hacking.pro; '",
"'/**/OR/**/1/**/=/**/1",
"AND (SELECT 1 FROM(SELECT COUNT(*),CONCAT('x',(SELECT (ELT(1=1,1))),'x',FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY x)a)"
).stream();
}
@ParameterizedTest @ParameterizedTest
@ValueSource(strings = { @MethodSource("badHostHeaderSource")
"Host: whatever.com:xxxx", public void testBadHostReject(String hostline)
"Host: myhost:testBadPort", {
"Host: a b c d", // whitespace in reg-name ByteBuffer buffer = BufferUtil.toBuffer(
"Host: a\to\tz", // tabs in reg-name "GET / HTTP/1.1\n" +
"Host: hosta, hostb, hostc", // spaces in reg-name "Host: " + hostline + "\n" +
"Host: [sd ajklf;d sajklf;d sajfkl;d]", // not a valid IPv6 address "Connection: close\n" +
"Host: hosta\nHost: hostb\nHost: hostc" // multi-line "\n");
})
public void testBadHost(String hostline) HttpParser.RequestHandler handler = new Handler();
HttpParser parser = new HttpParser(handler);
parser.parseNext(buffer);
assertThat(_bad, startsWith("Bad "));
}
@ParameterizedTest
@MethodSource("badHostHeaderSource")
public void testBadHostAllow(String hostline)
{
ByteBuffer buffer = BufferUtil.toBuffer(
"GET / HTTP/1.1\n" +
"Host: " + hostline + "\n" +
"Connection: close\n" +
"\n");
HttpParser.RequestHandler handler = new Handler();
HttpCompliance httpCompliance = HttpCompliance.from("RFC7230,UNSAFE_HOST_HEADER");
HttpParser parser = new HttpParser(handler, httpCompliance);
parser.parseNext(buffer);
assertNull(_bad);
assertNotNull(_host);
}
public static Stream<Arguments> duplicateHostHeadersSource()
{
return Stream.of(
// different values
Arguments.of("Host: hosta\nHost: hostb\nHost: hostc"),
// same values
Arguments.of("Host: foo\nHost: foo"),
// separated by another header
Arguments.of("Host: bar\nX-Zed: zed\nHost: bar")
);
}
@ParameterizedTest
@MethodSource("duplicateHostHeadersSource")
public void testDuplicateHostReject(String hostline)
{ {
ByteBuffer buffer = BufferUtil.toBuffer( ByteBuffer buffer = BufferUtil.toBuffer(
"GET / HTTP/1.1\n" + "GET / HTTP/1.1\n" +
@ -2062,7 +2144,25 @@ public class HttpParserTest
HttpParser.RequestHandler handler = new Handler(); HttpParser.RequestHandler handler = new Handler();
HttpParser parser = new HttpParser(handler); HttpParser parser = new HttpParser(handler);
parser.parseNext(buffer); parser.parseNext(buffer);
assertThat(_bad, startsWith("Bad")); assertThat(_bad, startsWith("Duplicate Host Header"));
}
@ParameterizedTest
@MethodSource("duplicateHostHeadersSource")
public void testDuplicateHostAllow(String hostline)
{
ByteBuffer buffer = BufferUtil.toBuffer(
"GET / HTTP/1.1\n" +
hostline + "\n" +
"Connection: close\n" +
"\n");
HttpParser.RequestHandler handler = new Handler();
HttpCompliance httpCompliance = HttpCompliance.from("RFC7230,DUPLICATE_HOST_HEADERS");
HttpParser parser = new HttpParser(handler, httpCompliance);
parser.parseNext(buffer);
assertNull(_bad);
assertNotNull(_host);
} }
@ParameterizedTest @ParameterizedTest

View File

@ -16,17 +16,38 @@ package org.eclipse.jetty.util;
import java.net.InetAddress; import java.net.InetAddress;
import org.eclipse.jetty.util.annotation.ManagedAttribute; import org.eclipse.jetty.util.annotation.ManagedAttribute;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* <p>Parse an authority string (in the form {@code host:port}) into * <p>Parse an authority string (in the form {@code host:port}) into
* {@code host} and {@code port}, handling IPv4 and IPv6 host formats * {@code host} and {@code port}, handling IPv4 and IPv6 host formats
* as defined in https://www.ietf.org/rfc/rfc2732.txt</p> * as defined in <a href="https://www.ietf.org/rfc/rfc2732.txt">RFC 2732</a></p>
*/ */
public class HostPort public class HostPort
{ {
private static final Logger LOG = LoggerFactory.getLogger(HostPort.class);
private static final int BAD_PORT = -1;
private final String _host; private final String _host;
private final int _port; private final int _port;
/**
* Create a HostPort from an unsafe (and not validated) authority.
*
* <p>
* There are no validations performed against the provided authority.
* It is quite possible to end up with HostPort that cannot be used
* to generate valid URL, URI, InetSocketAddress, Location header, etc.
* </p>
*
* @param authority raw authority
* @return the HostPort
*/
public static HostPort unsafe(String authority)
{
return new HostPort(authority, true);
}
public HostPort(String host, int port) public HostPort(String host, int port)
{ {
_host = normalizeHost(host); _host = normalizeHost(host);
@ -35,35 +56,83 @@ public class HostPort
public HostPort(String authority) throws IllegalArgumentException public HostPort(String authority) throws IllegalArgumentException
{ {
this(authority, false);
}
@SuppressWarnings({"ReassignedVariable", "DataFlowIssue"})
private HostPort(String authority, boolean unsafe)
{
String host;
//noinspection UnusedAssignment
int port = 0;
if (authority == null) if (authority == null)
throw new IllegalArgumentException("No Authority"); {
LOG.warn("Bad Authority [<null>]");
if (!unsafe)
throw new IllegalArgumentException("No Authority");
_host = "";
_port = 0;
return;
}
if (authority.isEmpty())
{
_host = authority;
_port = 0;
return;
}
try try
{ {
if (authority.isEmpty()) if (authority.charAt(0) == '[')
{
_host = authority;
_port = 0;
}
else if (authority.charAt(0) == '[')
{ {
// ipv6reference // ipv6reference
int close = authority.lastIndexOf(']'); int close = authority.lastIndexOf(']');
if (close < 0) if (close < 0)
throw new IllegalArgumentException("Bad IPv6 host"); {
_host = authority.substring(0, close + 1); LOG.warn("Bad IPv6 host: [{}]", authority);
if (!isValidIpAddress(_host)) if (!unsafe)
throw new IllegalArgumentException("Bad IPv6 host"); throw new IllegalArgumentException("Bad IPv6 host");
host = authority;
}
else
{
host = authority.substring(0, close + 1);
}
if (!isValidIpAddress(host))
{
LOG.warn("Bad IPv6 host: [{}]", host);
if (!unsafe)
throw new IllegalArgumentException("Bad IPv6 host");
}
if (authority.length() > close + 1) if (authority.length() > close + 1)
{ {
// ipv6 with port // ipv6 with port
if (authority.charAt(close + 1) != ':') if (authority.charAt(close + 1) != ':')
throw new IllegalArgumentException("Bad IPv6 port"); {
_port = parsePort(authority.substring(close + 2)); LOG.warn("Bad IPv6 port: [{}]", authority);
if (!unsafe)
throw new IllegalArgumentException("Bad IPv6 port");
host = authority; // whole authority (no substring)
port = 0; // no port
}
else
{
port = parsePort(authority.substring(close + 2), unsafe);
// horribly bad port during unsafe
if (unsafe && (port == BAD_PORT))
{
host = authority; // whole authority (no substring)
port = 0;
}
}
} }
else else
{ {
_port = 0; port = 0;
} }
} }
else else
@ -75,38 +144,76 @@ public class HostPort
if (c != authority.indexOf(':')) if (c != authority.indexOf(':'))
{ {
// ipv6address no port // ipv6address no port
_host = "[" + authority + "]"; port = 0;
if (!isValidIpAddress(_host)) host = "[" + authority + "]";
throw new IllegalArgumentException("Bad IPv6 host"); if (!isValidIpAddress(host))
_port = 0; {
LOG.warn("Bad IPv6Address: [{}]", host);
if (!unsafe)
throw new IllegalArgumentException("Bad IPv6 host");
host = authority; // whole authority (no substring)
}
} }
else else
{ {
// host/ipv4 with port // host/ipv4 with port
_host = authority.substring(0, c); host = authority.substring(0, c);
if (StringUtil.isBlank(_host) || !isValidHostName(_host)) if (StringUtil.isBlank(host))
throw new IllegalArgumentException("Bad Authority"); {
_port = parsePort(authority.substring(c + 1)); LOG.warn("Bad Authority: [{}]", host);
if (!unsafe)
throw new IllegalArgumentException("Bad Authority");
// unsafe - allow host to be empty
host = "";
}
else if (!isValidHostName(host))
{
LOG.warn("Bad Authority: [{}]", host);
if (!unsafe)
throw new IllegalArgumentException("Bad Authority");
// unsafe - bad hostname
host = authority; // whole authority (no substring)
}
port = parsePort(authority.substring(c + 1), unsafe);
// horribly bad port during unsafe
if (unsafe && (port == BAD_PORT))
{
host = authority; // whole authority (no substring)
port = 0;
}
} }
} }
else else
{ {
// host/ipv4 without port // host/ipv4 without port
_host = authority; host = authority;
if (StringUtil.isBlank(_host) || !isValidHostName(_host)) if (StringUtil.isBlank(host) || !isValidHostName(host))
throw new IllegalArgumentException("Bad Authority"); {
_port = 0; LOG.warn("Bad Authority: [{}]", host);
if (!unsafe)
throw new IllegalArgumentException("Bad Authority");
}
port = 0;
} }
} }
} }
catch (IllegalArgumentException iae) catch (IllegalArgumentException iae)
{ {
throw iae; if (!unsafe)
throw iae;
host = authority;
port = 0;
} }
catch (Exception ex) catch (Exception ex)
{ {
throw new IllegalArgumentException("Bad HostPort", ex); if (!unsafe)
throw new IllegalArgumentException("Bad HostPort", ex);
host = authority;
port = 0;
} }
_host = host;
_port = port;
} }
protected boolean isValidIpAddress(String ip) protected boolean isValidIpAddress(String ip)
@ -181,8 +288,8 @@ public class HostPort
} }
/** /**
* Normalizes IPv6 address as per https://tools.ietf.org/html/rfc2732 * Normalizes IPv6 address as per <a href="https://tools.ietf.org/html/rfc2732">RFC 2732</a>
* and https://tools.ietf.org/html/rfc6874, * and <a href="https://tools.ietf.org/html/rfc6874">RFC 6874</a>,
* surrounding with square brackets if they are absent. * surrounding with square brackets if they are absent.
* *
* @param host a host name, IPv4 address, IPv6 address or IPv6 literal * @param host a host name, IPv4 address, IPv6 address or IPv6 literal
@ -216,4 +323,43 @@ public class HostPort
return port; return port;
} }
/**
* Parse a potential port.
*
* @param rawPort the raw port string to parse
* @param unsafe true to always return a port in the range 0 to 65535 (or -1 for undefined if rawPort is horribly bad), false to return
* the provided port (or {@link IllegalArgumentException} if it is horribly bad)
* @return the port
* @throws IllegalArgumentException if unable to parse a valid port and {@code unsafe} is false
*/
private int parsePort(String rawPort, boolean unsafe)
{
if (StringUtil.isEmpty(rawPort))
{
if (!unsafe)
throw new IllegalArgumentException("Bad port [" + rawPort + "]");
return 0;
}
try
{
int port = Integer.parseInt(rawPort);
if (port <= 0 || port > 65535)
{
LOG.warn("Bad port [{}]", port);
if (!unsafe)
throw new IllegalArgumentException("Bad port");
return BAD_PORT;
}
return port;
}
catch (NumberFormatException e)
{
LOG.warn("Bad port [{}]", rawPort);
if (!unsafe)
throw new IllegalArgumentException("Bad Port");
return BAD_PORT;
}
}
} }

View File

@ -21,95 +21,122 @@ import org.junit.jupiter.params.provider.MethodSource;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
public class HostPortTest public class HostPortTest
{ {
public static Stream<Arguments> validAuthorityProvider() public static Stream<Arguments> validAuthorityProvider()
{ {
return Stream.of( return Stream.of(
Arguments.of("", "", null), Arguments.of("", "", 0),
Arguments.of("host", "host", null), Arguments.of("host", "host", 0),
Arguments.of("host:80", "host", "80"), Arguments.of("host:80", "host", 80),
Arguments.of("10.10.10.1", "10.10.10.1", null), Arguments.of("10.10.10.1", "10.10.10.1", 0),
Arguments.of("10.10.10.1:80", "10.10.10.1", "80"), Arguments.of("10.10.10.1:80", "10.10.10.1", 80),
Arguments.of("127.0.0.1:65535", "127.0.0.1", "65535"), Arguments.of("127.0.0.1:65535", "127.0.0.1", 65535),
// Localhost tests // Localhost tests
Arguments.of("localhost:80", "localhost", "80"), Arguments.of("localhost:80", "localhost", 80),
Arguments.of("127.0.0.1:80", "127.0.0.1", "80"), Arguments.of("127.0.0.1:80", "127.0.0.1", 80),
Arguments.of("::1", "[::1]", null), Arguments.of("::1", "[::1]", 0),
Arguments.of("[::1]:443", "[::1]", "443"), Arguments.of("[::1]:443", "[::1]", 443),
// Examples from https://tools.ietf.org/html/rfc2732#section-2 // Examples from https://tools.ietf.org/html/rfc2732#section-2
Arguments.of("[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80", "[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]", "80"), Arguments.of("[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80", "[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]", 80),
Arguments.of("[1080:0:0:0:8:800:200C:417A]", "[1080:0:0:0:8:800:200C:417A]", null), Arguments.of("[1080:0:0:0:8:800:200C:417A]", "[1080:0:0:0:8:800:200C:417A]", 0),
Arguments.of("[3ffe:2a00:100:7031::1]", "[3ffe:2a00:100:7031::1]", null), Arguments.of("[3ffe:2a00:100:7031::1]", "[3ffe:2a00:100:7031::1]", 0),
Arguments.of("[1080::8:800:200C:417A]", "[1080::8:800:200C:417A]", null), Arguments.of("[1080::8:800:200C:417A]", "[1080::8:800:200C:417A]", 0),
Arguments.of("[::192.9.5.5]", "[::192.9.5.5]", null), Arguments.of("[::192.9.5.5]", "[::192.9.5.5]", 0),
Arguments.of("[::FFFF:129.144.52.38]:80", "[::FFFF:129.144.52.38]", "80"), Arguments.of("[::FFFF:129.144.52.38]:80", "[::FFFF:129.144.52.38]", 80),
Arguments.of("[2010:836B:4179::836B:4179]", "[2010:836B:4179::836B:4179]", null), Arguments.of("[2010:836B:4179::836B:4179]", "[2010:836B:4179::836B:4179]", 0),
// Modified Examples from above, not using square brackets (valid, but should never have a port) // Modified Examples from above, not using square brackets (valid, but should never have a port)
Arguments.of("FEDC:BA98:7654:3210:FEDC:BA98:7654:3210", "[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]", null), Arguments.of("FEDC:BA98:7654:3210:FEDC:BA98:7654:3210", "[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]", 0),
Arguments.of("1080:0:0:0:8:800:200C:417A", "[1080:0:0:0:8:800:200C:417A]", null), Arguments.of("1080:0:0:0:8:800:200C:417A", "[1080:0:0:0:8:800:200C:417A]", 0),
Arguments.of("3ffe:2a00:100:7031::1", "[3ffe:2a00:100:7031::1]", null), Arguments.of("3ffe:2a00:100:7031::1", "[3ffe:2a00:100:7031::1]", 0),
Arguments.of("1080::8:800:200C:417A", "[1080::8:800:200C:417A]", null), Arguments.of("1080::8:800:200C:417A", "[1080::8:800:200C:417A]", 0),
Arguments.of("::192.9.5.5", "[::192.9.5.5]", null), Arguments.of("::192.9.5.5", "[::192.9.5.5]", 0),
Arguments.of("::FFFF:129.144.52.38", "[::FFFF:129.144.52.38]", null), Arguments.of("::FFFF:129.144.52.38", "[::FFFF:129.144.52.38]", 0),
Arguments.of("2010:836B:4179::836B:4179", "[2010:836B:4179::836B:4179]", null) Arguments.of("2010:836B:4179::836B:4179", "[2010:836B:4179::836B:4179]", 0)
); );
} }
@ParameterizedTest
@MethodSource("validAuthorityProvider")
public void testValidAuthority(String authority, String expectedHost, int expectedPort)
{
HostPort hostPort = new HostPort(authority);
assertThat("Host for: " + authority, hostPort.getHost(), is(expectedHost));
assertThat("Port for: " + authority, hostPort.getPort(), is(expectedPort));
}
@ParameterizedTest
@MethodSource("validAuthorityProvider")
public void testValidAuthorityViaUnsafe(String authority, String expectedHost, Integer expectedPort)
{
HostPort hostPort = HostPort.unsafe(authority);
assertThat("(unsafe) Host for: " + authority, hostPort.getHost(), is(expectedHost));
assertThat("(unsafe) Port for: " + authority, hostPort.getPort(), is(expectedPort));
}
public static Stream<Arguments> invalidAuthorityProvider() public static Stream<Arguments> invalidAuthorityProvider()
{ {
return Stream.of( return Stream.of(
null, Arguments.of(null, "", 0), // null authority
":80", // no host, port only Arguments.of(":", "", 0), // no host, no port, port delimiter only
"host:", // no port Arguments.of(":::::", ":::::", 0), // host is only port delimiters, no port, port delimiter only
"127.0.0.1:", // no port Arguments.of(":80", "", 80), // no host, port only
"[0::0::0::0::1]:", // no port Arguments.of("::::::80", "::::::80", 0), // no host, port only
"[0::0::0::1]", // not valid to Java (InetAddress, InetSocketAddress, or URI) : "Expected hex digits or IPv4 address" Arguments.of("host:", "host", 0), // host, port delimiter, but empty
"[0::0::0::1]:80", // not valid to Java (InetAddress, InetSocketAddress, or URI) : "Expected hex digits or IPv4 address" Arguments.of("host:::::", "host:::::", 0), // host, port delimiter, but empty
"0:1:2:3:4:5:6", // not valid to Java (InetAddress, InetSocketAddress, or URI) : "IPv6 address too short" Arguments.of("127.0.0.1:", "127.0.0.1", 0), // IPv4, port delimiter, but empty
"host:xxx", // invalid port Arguments.of("[0::0::0::0::1", "[0::0::0::0::1", 0), // no ending bracket for IP literal
"127.0.0.1:xxx", // host + invalid port Arguments.of("0::0::0::0::1]", "0::0::0::0::1]", 0), // no starting bracket for IP literal
"[0::0::0::0::1]:xxx", // ipv6 + invalid port Arguments.of("[0::0::0::0::1]:", "[0::0::0::0::1]", 0), // IP literal, port delimiter, but empty
"host:-80", // host + invalid port // forbidden characters in reg-name
"127.0.0.1:-80", // ipv4 + invalid port Arguments.of("\"\"", "\"\"", 0), // just quotes
"[0::0::0::0::1]:-80", // ipv6 + invalid port // not valid to Java (InetAddress, InetSocketAddress, or URI) : "Expected hex digits or IPv4 address"
"127.0.0.1:65536" // ipv4 + port value too high Arguments.of("[0::0::0::1]", "[0::0::0::1]", 0),
).map(Arguments::of); Arguments.of("[0::0::0::1]:80", "[0::0::0::1]", 80),
} // not valid to Java (InetAddress, InetSocketAddress, or URI) : "IPv6 address too short"
Arguments.of("0:1:2:3:4:5:6", "0:1:2:3:4:5:6", 0),
@ParameterizedTest // Bad ports declarations (should all end up with -1 port)
@MethodSource("validAuthorityProvider") Arguments.of("host:xxx", "host:xxx", 0), // invalid port
public void testValidAuthority(String authority, String expectedHost, Integer expectedPort) Arguments.of("127.0.0.1:xxx", "127.0.0.1:xxx", 0), // host + invalid port
{ Arguments.of("[0::0::0::0::1]:xxx", "[0::0::0::0::1]:xxx", 0), // ipv6 + invalid port
try Arguments.of("[0::0::0::0::1].80", "[0::0::0::0::1].80", 0), // ipv6 with bogus port delimiter
{ Arguments.of("host:-80", "host:-80", 0), // host + invalid negative port
HostPort hostPort = new HostPort(authority); Arguments.of("127.0.0.1:-80", "127.0.0.1:-80", 0), // ipv4 + invalid port
assertThat(authority, hostPort.getHost(), is(expectedHost)); Arguments.of("[0::0::0::0::1]:-80", "[0::0::0::0::1]:-80", 0), // ipv6 + invalid port
Arguments.of("127.0.0.1:65536", "127.0.0.1:65536", 0), // ipv4 + port value too high
if (expectedPort == null) Arguments.of("example.org:112233445566778899", "example.org:112233445566778899", 0), // ipv4 + port value too high
assertThat(authority, hostPort.getPort(), is(0)); // Examples of bad Host header values (usually client bugs that shouldn't allow them to be sent)
else Arguments.of("Group - Machine", "Group - Machine", 0), // spaces
assertThat(authority, hostPort.getPort(), is(expectedPort)); Arguments.of("<calculated when request is sent>", "<calculated when request is sent>", 0), // spaces and forbidden characters in reg-name
} Arguments.of("[link](https://example.org/)", "[link](https://example.org/)", 0), // forbidden characters in reg-name
catch (Exception e) Arguments.of("example.org/zed", "example.org/zed", 0), // forbidden character in reg-name (slash)
{ // common hacking attempts, seen as values on the `Host:` request header
if (expectedHost != null) Arguments.of("| ping 127.0.0.1 -n 10", "| ping 127.0.0.1 -n 10", 0), // forbidden characters in reg-name
e.printStackTrace(); Arguments.of("%uf%80%ff%xx%uffff", "%uf%80%ff%xx%uffff", 0), // (invalid encoding)
assertNull(authority, expectedHost); Arguments.of("[${jndi${:-:}ldap${:-:}]", "[${jndi${:-:}ldap${:-:}]", 0), // log4j hacking (forbidden chars in reg-name)
} Arguments.of("[${jndi:ldap://example.org:59377/nessus}]", "[${jndi:ldap://example.org:59377/nessus}]", 0), // log4j hacking (forbidden chars in reg-name)
Arguments.of("${ip}", "${ip}", 0), // variation of log4j hack (forbidden chars in reg-name)
Arguments.of("' *; host xyz.hacking.pro; '", "' *; host xyz.hacking.pro; '", 0), // forbidden chars in reg-name
Arguments.of("'/**/OR/**/1/**/=/**/1", "'/**/OR/**/1/**/=/**/1", 0), // forbidden chars in reg-name
Arguments.of("AND (SELECT 1 FROM(SELECT COUNT(*),CONCAT('x',(SELECT (ELT(1=1,1))),'x',FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY x)a)", "AND (SELECT 1 FROM(SELECT COUNT(*),CONCAT('x',(SELECT (ELT(1=1,1))),'x',FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY x)a)", 0)
);
} }
@ParameterizedTest @ParameterizedTest
@MethodSource("invalidAuthorityProvider") @MethodSource("invalidAuthorityProvider")
public void testInvalidAuthority(String authority) public void testInvalidAuthority(String rawAuthority, String ignoredExpectedHost, int ignoredExpectedPort)
{ {
assertThrows(IllegalArgumentException.class, () -> assertThrows(IllegalArgumentException.class, () -> new HostPort(rawAuthority));
{ }
new HostPort(authority);
}); @ParameterizedTest
@MethodSource("invalidAuthorityProvider")
public void testInvalidAuthorityViaUnsafe(String rawAuthority, String expectedHost, int expectedPort)
{
HostPort hostPort = HostPort.unsafe(rawAuthority);
assertThat("(unsafe) Host for: " + rawAuthority, hostPort.getHost(), is(expectedHost));
assertThat("(unsafe) Port for: " + rawAuthority, hostPort.getPort(), is(expectedPort));
} }
} }