From 016de2faebdd9ac90f00df9ac4bc7c74193e68e6 Mon Sep 17 00:00:00 2001 From: Joakim Erdfelt Date: Fri, 3 Feb 2023 08:30:07 -0600 Subject: [PATCH] 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 --- .../eclipse/jetty/http/HttpCompliance.java | 16 +- .../org/eclipse/jetty/http/HttpParser.java | 37 +++- .../eclipse/jetty/http/HttpParserTest.java | 122 +++++++++- .../java/org/eclipse/jetty/util/HostPort.java | 208 +++++++++++++++--- .../org/eclipse/jetty/util/HostPortTest.java | 165 ++++++++------ 5 files changed, 427 insertions(+), 121 deletions(-) diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCompliance.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCompliance.java index 2f4b1d17671..8e0a47d6924 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCompliance.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCompliance.java @@ -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 RFC 7230: Section 5.4, 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 RFC 7230, 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; diff --git a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java index 53472d09654..7e802c62824 100644 --- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java +++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java @@ -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; } diff --git a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java index 2ae00ca1228..c2bad0b9cdc 100644 --- a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java +++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java @@ -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 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 + "", + "[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 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 diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/HostPort.java b/jetty-util/src/main/java/org/eclipse/jetty/util/HostPort.java index 5374b437e37..61e99c9625b 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/HostPort.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/HostPort.java @@ -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; /** *

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

+ * as defined in RFC 2732

*/ 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. + * + *

+ * 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. + *

+ * + * @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 []"); + 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 RFC 2732 + * and RFC 6874, * 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; + } + } } diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/HostPortTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/HostPortTest.java index 7b5df5d18f0..f2552cd847c 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/HostPortTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/HostPortTest.java @@ -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 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 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("", "", 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)); } }