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));
}
}