Skip to content

Commit

Permalink
Jetty 10 - Configurable Unsafe Host Header (#9283)
Browse files Browse the repository at this point in the history
* Adding HttpCompliance.DUPLICATE_HOST_HEADERS
  + Optional compliance that allowance duplicate host headers.
* Adding HttpCompliance.UNSAFE_HOST_HEADER
  + Optional compliance that allows unsafe host headers.
* Adding warning logging for bad Host / authority situations

Signed-off-by: Joakim Erdfelt <joakim.erdfelt@gmail.com>
  • Loading branch information
joakime committed Feb 3, 2023
1 parent f0cba08 commit 016de2f
Show file tree
Hide file tree
Showing 5 changed files with 423 additions and 117 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,21 @@ public enum Violation implements ComplianceViolation
* 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;
Expand Down
37 changes: 28 additions & 9 deletions jetty-http/src/main/java/org/eclipse/jetty/http/HttpParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

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;
Expand All @@ -33,10 +34,12 @@
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;

/**
Expand Down Expand Up @@ -226,7 +229,7 @@ public enum State
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;
Expand Down Expand Up @@ -1028,14 +1031,28 @@ else if (_endOfContent == EndOfContent.CHUNKED_CONTENT)
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;
Expand Down Expand Up @@ -1072,6 +1089,8 @@ else if (_endOfContent == EndOfContent.CHUNKED_CONTENT)
_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));
}

Expand Down Expand Up @@ -1183,7 +1202,7 @@ protected boolean parseFields(ByteBuffer buffer)
}

// 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");
}
Expand Down Expand Up @@ -1888,7 +1907,7 @@ public void reset()
_responseStatus = 0;
_contentChunk = null;
_headerBytes = 0;
_host = false;
_parsedHost = null;
_headerComplete = false;
}

Expand Down
122 changes: 111 additions & 11 deletions jetty-http/src/test/java/org/eclipse/jetty/http/HttpParserTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
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;
Expand All @@ -28,6 +29,8 @@
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;
Expand All @@ -39,6 +42,7 @@
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;
Expand Down Expand Up @@ -2041,17 +2045,95 @@ public void testHostPort()
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" +
Expand All @@ -2062,7 +2144,25 @@ public void testBadHost(String hostline)
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
Expand Down

0 comments on commit 016de2f

Please sign in to comment.