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.
* 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 description;

View File

@ -24,6 +24,7 @@ import java.util.Map;
import org.eclipse.jetty.http.HttpTokens.EndOfContent;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.Index;
import org.eclipse.jetty.util.StringUtil;
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.Violation;
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.MULTIPLE_CONTENT_LENGTHS;
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.UNSAFE_HOST_HEADER;
import static org.eclipse.jetty.http.HttpCompliance.Violation.WHITESPACE_AFTER_FIELD_NAME;
/**
@ -226,7 +229,7 @@ public class HttpParser
private String _valueString;
private int _responseStatus;
private int _headerBytes;
private boolean _host;
private String _parsedHost;
private boolean _headerComplete;
private volatile State _state = State.START;
private volatile FieldState _fieldState = FieldState.FIELD;
@ -1028,14 +1031,28 @@ public class HttpParser
break;
case HOST:
if (_host)
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad Host: multiple headers");
_host = true;
if (_parsedHost != null)
{
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())
{
_field = new HostPortHttpField(_header,
CASE_SENSITIVE_FIELD_NAME.isAllowedBy(_complianceMode) ? _headerString : _header.asString(),
_valueString);
HostPort hostPort;
if (UNSAFE_HOST_HEADER.isAllowedBy(_complianceMode))
{
_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();
}
break;
@ -1072,6 +1089,8 @@ public class HttpParser
_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));
}
@ -1183,7 +1202,7 @@ public class HttpParser
}
// 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");
}
@ -1888,7 +1907,7 @@ public class HttpParser
_responseStatus = 0;
_contentChunk = null;
_headerBytes = 0;
_host = false;
_parsedHost = null;
_headerComplete = false;
}

View File

@ -18,6 +18,7 @@ import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.stream.Stream;
import org.eclipse.jetty.http.HttpParser.State;
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.Test;
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 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.empty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -2041,17 +2045,95 @@ public class HttpParserTest
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
@ValueSource(strings = {
"Host: whatever.com:xxxx",
"Host: myhost:testBadPort",
"Host: a b c d", // whitespace in reg-name
"Host: a\to\tz", // tabs in reg-name
"Host: hosta, hostb, hostc", // spaces in reg-name
"Host: [sd ajklf;d sajklf;d sajfkl;d]", // not a valid IPv6 address
"Host: hosta\nHost: hostb\nHost: hostc" // multi-line
})
public void testBadHost(String hostline)
@MethodSource("badHostHeaderSource")
public void testBadHostReject(String hostline)
{
ByteBuffer buffer = BufferUtil.toBuffer(
"GET / HTTP/1.1\n" +
"Host: " + hostline + "\n" +
"Connection: close\n" +
"\n");
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(
"GET / HTTP/1.1\n" +
@ -2062,7 +2144,25 @@ public class HttpParserTest
HttpParser.RequestHandler handler = new Handler();
HttpParser parser = new HttpParser(handler);
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

View File

@ -16,17 +16,38 @@ package org.eclipse.jetty.util;
import java.net.InetAddress;
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
* {@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
{
private static final Logger LOG = LoggerFactory.getLogger(HostPort.class);
private static final int BAD_PORT = -1;
private final String _host;
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)
{
_host = normalizeHost(host);
@ -35,35 +56,83 @@ public class HostPort
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)
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
{
if (authority.isEmpty())
{
_host = authority;
_port = 0;
}
else if (authority.charAt(0) == '[')
if (authority.charAt(0) == '[')
{
// ipv6reference
int close = authority.lastIndexOf(']');
if (close < 0)
throw new IllegalArgumentException("Bad IPv6 host");
_host = authority.substring(0, close + 1);
if (!isValidIpAddress(_host))
throw new IllegalArgumentException("Bad IPv6 host");
{
LOG.warn("Bad IPv6 host: [{}]", authority);
if (!unsafe)
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)
{
// ipv6 with port
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
{
_port = 0;
port = 0;
}
}
else
@ -75,38 +144,76 @@ public class HostPort
if (c != authority.indexOf(':'))
{
// ipv6address no port
_host = "[" + authority + "]";
if (!isValidIpAddress(_host))
throw new IllegalArgumentException("Bad IPv6 host");
_port = 0;
port = 0;
host = "[" + authority + "]";
if (!isValidIpAddress(host))
{
LOG.warn("Bad IPv6Address: [{}]", host);
if (!unsafe)
throw new IllegalArgumentException("Bad IPv6 host");
host = authority; // whole authority (no substring)
}
}
else
{
// host/ipv4 with port
_host = authority.substring(0, c);
if (StringUtil.isBlank(_host) || !isValidHostName(_host))
throw new IllegalArgumentException("Bad Authority");
_port = parsePort(authority.substring(c + 1));
host = authority.substring(0, c);
if (StringUtil.isBlank(host))
{
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
{
// host/ipv4 without port
_host = authority;
if (StringUtil.isBlank(_host) || !isValidHostName(_host))
throw new IllegalArgumentException("Bad Authority");
_port = 0;
host = authority;
if (StringUtil.isBlank(host) || !isValidHostName(host))
{
LOG.warn("Bad Authority: [{}]", host);
if (!unsafe)
throw new IllegalArgumentException("Bad Authority");
}
port = 0;
}
}
}
catch (IllegalArgumentException iae)
{
throw iae;
if (!unsafe)
throw iae;
host = authority;
port = 0;
}
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)
@ -181,8 +288,8 @@ public class HostPort
}
/**
* Normalizes IPv6 address as per https://tools.ietf.org/html/rfc2732
* and https://tools.ietf.org/html/rfc6874,
* Normalizes IPv6 address as per <a href="https://tools.ietf.org/html/rfc2732">RFC 2732</a>
* and <a href="https://tools.ietf.org/html/rfc6874">RFC 6874</a>,
* surrounding with square brackets if they are absent.
*
* @param host a host name, IPv4 address, IPv6 address or IPv6 literal
@ -216,4 +323,43 @@ public class HostPort
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.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class HostPortTest
{
public static Stream<Arguments> validAuthorityProvider()
{
return Stream.of(
Arguments.of("", "", null),
Arguments.of("host", "host", null),
Arguments.of("host:80", "host", "80"),
Arguments.of("10.10.10.1", "10.10.10.1", null),
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("", "", 0),
Arguments.of("host", "host", 0),
Arguments.of("host:80", "host", 80),
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("127.0.0.1:65535", "127.0.0.1", 65535),
// Localhost tests
Arguments.of("localhost:80", "localhost", "80"),
Arguments.of("127.0.0.1:80", "127.0.0.1", "80"),
Arguments.of("::1", "[::1]", null),
Arguments.of("[::1]:443", "[::1]", "443"),
Arguments.of("localhost:80", "localhost", 80),
Arguments.of("127.0.0.1:80", "127.0.0.1", 80),
Arguments.of("::1", "[::1]", 0),
Arguments.of("[::1]:443", "[::1]", 443),
// 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("[1080:0:0:0:8:800:200C:417A]", "[1080:0:0:0:8:800:200C:417A]", null),
Arguments.of("[3ffe:2a00:100:7031::1]", "[3ffe:2a00:100:7031::1]", null),
Arguments.of("[1080::8:800:200C:417A]", "[1080::8:800:200C:417A]", null),
Arguments.of("[::192.9.5.5]", "[::192.9.5.5]", null),
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("[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]", 0),
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]", 0),
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("[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)
Arguments.of("FEDC:BA98:7654:3210:FEDC:BA98:7654:3210", "[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]", null),
Arguments.of("1080:0:0:0:8:800:200C:417A", "[1080:0:0:0:8:800:200C:417A]", null),
Arguments.of("3ffe:2a00:100:7031::1", "[3ffe:2a00:100:7031::1]", null),
Arguments.of("1080::8:800:200C:417A", "[1080::8:800:200C:417A]", null),
Arguments.of("::192.9.5.5", "[::192.9.5.5]", null),
Arguments.of("::FFFF:129.144.52.38", "[::FFFF:129.144.52.38]", null),
Arguments.of("2010:836B:4179::836B:4179", "[2010:836B:4179::836B:4179]", 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]", 0),
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]", 0),
Arguments.of("::192.9.5.5", "[::192.9.5.5]", 0),
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]", 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()
{
return Stream.of(
null,
":80", // no host, port only
"host:", // no port
"127.0.0.1:", // no port
"[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
).map(Arguments::of);
}
@ParameterizedTest
@MethodSource("validAuthorityProvider")
public void testValidAuthority(String authority, String expectedHost, Integer expectedPort)
{
try
{
HostPort hostPort = new HostPort(authority);
assertThat(authority, hostPort.getHost(), is(expectedHost));
if (expectedPort == null)
assertThat(authority, hostPort.getPort(), is(0));
else
assertThat(authority, hostPort.getPort(), is(expectedPort));
}
catch (Exception e)
{
if (expectedHost != null)
e.printStackTrace();
assertNull(authority, expectedHost);
}
Arguments.of(null, "", 0), // null authority
Arguments.of(":", "", 0), // no host, no port, port delimiter only
Arguments.of(":::::", ":::::", 0), // host is only port delimiters, no port, port delimiter only
Arguments.of(":80", "", 80), // no host, port only
Arguments.of("::::::80", "::::::80", 0), // no host, port only
Arguments.of("host:", "host", 0), // host, port delimiter, but empty
Arguments.of("host:::::", "host:::::", 0), // host, port delimiter, but empty
Arguments.of("127.0.0.1:", "127.0.0.1", 0), // IPv4, port delimiter, but empty
Arguments.of("[0::0::0::0::1", "[0::0::0::0::1", 0), // no ending bracket for IP literal
Arguments.of("0::0::0::0::1]", "0::0::0::0::1]", 0), // no starting bracket for IP literal
Arguments.of("[0::0::0::0::1]:", "[0::0::0::0::1]", 0), // IP literal, port delimiter, but empty
// forbidden characters in reg-name
Arguments.of("\"\"", "\"\"", 0), // just quotes
// not valid to Java (InetAddress, InetSocketAddress, or URI) : "Expected hex digits or IPv4 address"
Arguments.of("[0::0::0::1]", "[0::0::0::1]", 0),
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),
// Bad ports declarations (should all end up with -1 port)
Arguments.of("host:xxx", "host:xxx", 0), // invalid port
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
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
Arguments.of("127.0.0.1:-80", "127.0.0.1:-80", 0), // ipv4 + invalid port
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
Arguments.of("example.org:112233445566778899", "example.org:112233445566778899", 0), // ipv4 + port value too high
// Examples of bad Host header values (usually client bugs that shouldn't allow them to be sent)
Arguments.of("Group - Machine", "Group - Machine", 0), // spaces
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
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
Arguments.of("| ping 127.0.0.1 -n 10", "| ping 127.0.0.1 -n 10", 0), // forbidden characters in reg-name
Arguments.of("%uf%80%ff%xx%uffff", "%uf%80%ff%xx%uffff", 0), // (invalid encoding)
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
@MethodSource("invalidAuthorityProvider")
public void testInvalidAuthority(String authority)
public void testInvalidAuthority(String rawAuthority, String ignoredExpectedHost, int ignoredExpectedPort)
{
assertThrows(IllegalArgumentException.class, () ->
{
new HostPort(authority);
});
assertThrows(IllegalArgumentException.class, () -> new HostPort(rawAuthority));
}
@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));
}
}