Merge remote-tracking branch 'origin/jetty-10.0.x' into jetty-11.0.x
This commit is contained in:
commit
a13a932335
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue