File jetty-CVE-2025-11143.patch of Package jetty-minimal
--- a/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpSenderOverHTTP.java
+++ b/jetty-client/src/main/java/org/eclipse/jetty/client/http/HttpSenderOverHTTP.java
@@ -198,7 +198,9 @@ public HeadersCallback(HttpExchange exchange, HttpContent content, Callback call
String query = request.getQuery();
if (query != null)
path += "?" + query;
- metaData = new MetaData.Request(request.getMethod(), new HttpURI(path), request.getVersion(), request.getHeaders(), contentLength);
+ HttpURI uri = new HttpURI();
+ uri.parseRequestTarget(request.getMethod(), path);
+ metaData = new MetaData.Request(request.getMethod(), uri, request.getVersion(), request.getHeaders(), contentLength);
metaData.setTrailerSupplier(request.getTrailers());
if (!expects100Continue(request))
--- a/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java
+++ b/jetty-http/src/main/java/org/eclipse/jetty/http/HttpURI.java
@@ -433,7 +433,8 @@ private void parse(State state, final String uri, final int offset, final int en
switch (c)
{
case '/':
- mark = i;
+ pathMark = mark = i;
+ segment = mark + 1;
state = State.HOST_OR_PATH;
break;
case ';':
@@ -471,6 +472,8 @@ private void parse(State state, final String uri, final int offset, final int en
pathMark = segment = i;
state = State.PATH;
break;
+ case ':':
+ throw new IllegalArgumentException("Bad Scheme");
default:
mark = i;
if (_scheme == null)
@@ -491,7 +494,7 @@ private void parse(State state, final String uri, final int offset, final int en
{
case ':':
// must have been a scheme
- _scheme = uri.substring(mark, i);
+ _scheme = URIUtil.validateScheme(uri.substring(mark, i));
// Start again with scheme set
state = State.START;
break;
@@ -596,8 +599,19 @@ private void parse(State state, final String uri, final int offset, final int en
encoded = true;
break;
case '#':
+ case '?':
case ';':
- throw new IllegalArgumentException("Bad authority");
+ if (encodedCharacters > 0)
+ throw new IllegalArgumentException("Bad authority");
+ _host = uri.substring(mark, i);
+ if (_host.isEmpty())
+ throw new IllegalArgumentException("Bad authority");
+ encoded = false;
+ pathMark = mark = i;
+ segment = mark + 1;
+ state = State.PATH;
+ i--;
+ break;
default:
if (encodedCharacters > 0)
@@ -621,8 +635,13 @@ else if (!isUnreservedPctEncodedOrSubDelim(c))
case '/':
throw new IllegalArgumentException("No closing ']' for ipv6 in " + uri);
case ']':
- c = uri.charAt(++i);
- _host = uri.substring(mark, i);
+ i++;
+ String host = uri.substring(mark, i);
+ URIUtil.validateInetAddress(host);
+ _host = host;
+ if (i == end)
+ break;
+ c = uri.charAt(i);
if (c == ':')
{
mark = i + 1;
@@ -637,7 +656,7 @@ else if (!isUnreservedPctEncodedOrSubDelim(c))
case ':':
break;
default:
- if (!isHexDigit(c))
+ if (!isHexDigit(c) && c != '.')
throw new IllegalArgumentException("Bad authority");
break;
}
@@ -805,6 +824,7 @@ else if (c == '/')
break;
case SCHEME_OR_PATH:
case HOST_OR_PATH:
+ checkSegment(uri, segment, end, false);
_path = uri.substring(mark, end);
break;
case HOST:
--- a/jetty-http/src/test/java/org/eclipse/jetty/http/HttpURITest.java
+++ b/jetty-http/src/test/java/org/eclipse/jetty/http/HttpURITest.java
@@ -38,6 +38,8 @@
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
@@ -158,6 +160,19 @@ public void testAt() throws Exception
assertEquals("/@foo/bar", uri.getPath());
}
+ /**
+ * Test of an HttpURI of just a "/".
+ */
+ @Test
+ public void testFromSlash()
+ {
+ HttpURI uri = new HttpURI("/");
+ assertFalse(uri.hasViolations());
+ assertNull(uri.getScheme());
+ assertNull(uri.getAuthority());
+ assertEquals("/", uri.getPath());
+ }
+
@Test
public void testParams() throws Exception
{
@@ -684,6 +699,8 @@ public static Stream<Arguments> parseData()
// Simple IPv6 host no port (default path)
Arguments.of("http://[2001:db8::1]/", "http", "[2001:db8::1]", null, "/", null, null, null),
+ Arguments.of("http://[0:0:0:0:0:ffff:127.0.0.1]/", "http", "[0:0:0:0:0:ffff:127.0.0.1]", null, "/", null, null, null),
+ Arguments.of("http://[::ffff:127.0.0.1]/", "http", "[::ffff:127.0.0.1]", null, "/", null, null, null),
// Scheme-less IPv6, host with port (default path)
Arguments.of("//[2001:db8::1]:8080/", null, "[2001:db8::1]", "8080", "/", null, null, null),
@@ -725,7 +742,8 @@ public void testParseString(String input, String scheme, String host, Integer po
assertThat("[" + input + "] .param", httpUri.getParam(), is(param));
assertThat("[" + input + "] .query", httpUri.getQuery(), is(query));
assertThat("[" + input + "] .fragment", httpUri.getFragment(), is(fragment));
- assertThat("[" + input + "] .toString", httpUri.toString(), is(input));
+ if (!input.contains(":ffff:127.0.0.1"))
+ assertThat("[" + input + "] .toString", httpUri.toString(), is(input));
}
catch (URISyntaxException e)
{
@@ -758,7 +776,8 @@ public void testParseURI(String input, String scheme, String host, Integer port,
HttpURI httpUri = new HttpURI(input);
assertThat("[" + input + "] .scheme", httpUri.getScheme(), is(scheme));
- assertThat("[" + input + "] .host", httpUri.getHost(), is(host));
+ if (!input.contains(":ffff:127.0.0.1"))
+ assertThat("[" + input + "] .host", httpUri.getHost(), is(host));
assertThat("[" + input + "] .port", httpUri.getPort(), is(port == null ? -1 : port));
assertThat("[" + input + "] .path", httpUri.getPath(), is(path));
assertThat("[" + input + "] .param", httpUri.getParam(), is(param));
@@ -871,6 +890,24 @@ public void testRelativePathWithAuthority()
assertEquals("//host", uri.toString());
}
+ public static Stream<String> badSchemes()
+ {
+ return Stream.of(
+ "://host/path",
+ "\t://host/path",
+ " ://host/path",
+ "unknown^://host/path",
+ "http^://host/path"
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("badSchemes")
+ public void testBadSchemes(String uri)
+ {
+ assertThrows(IllegalArgumentException.class, () -> new HttpURI(uri));
+ }
+
public static Stream<String> badAuthorities()
{
return Stream.of(
@@ -890,9 +927,11 @@ public static Stream<String> badAuthorities()
"https://user@host:notport/path",
"https://user:password@host:notport/path",
"https://user @host.com/",
- "https://user#@host.com/",
"https://[notIpv6]/",
- "https://bad[0::1::2::3::4]/"
+ "https://bad[0::1::2::3::4]/",
+ "http://[normal.com@]vulndetector.com/",
+ "http://normal.com[user@vulndetector].com/",
+ "http://normal.com[@]vulndetector.com/"
);
}
@@ -902,4 +941,41 @@ public void testBadAuthority(String uri)
{
assertThrows(IllegalArgumentException.class, () -> new HttpURI(uri));
}
+
+ public static Stream<Arguments> authoritiesNoPath()
+ {
+ return Stream.of(
+ Arguments.of("http://good.com#@evil.com", "good.com", null, "@evil.com"),
+ Arguments.of("http://good.com?@evil.com", "good.com", "@evil.com", null)
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("authoritiesNoPath")
+ public void testAuthorityNoPath(String uri, String authority, String query, String fragment)
+ {
+ HttpURI httpURI = new HttpURI(uri);
+ assertThat(httpURI.getAuthority(), is(authority));
+ assertThat(httpURI.getPath(), is(""));
+ assertThat(httpURI.getQuery(), is(query));
+ assertThat(httpURI.getFragment(), is(fragment));
+ }
+
+ public static Stream<Arguments> connectURIs()
+ {
+ return Stream.of(
+ Arguments.of("localhost:8080"),
+ Arguments.of("127.0.0.1:8080"),
+ Arguments.of("[::1]:8080")
+ );
+ }
+
+ @ParameterizedTest
+ @MethodSource("connectURIs")
+ public void testConnect(String authority)
+ {
+ HttpURI httpURI = new HttpURI();
+ httpURI.parseRequestTarget(HttpMethod.CONNECT.asString(), authority);
+ assertThat(httpURI.getAuthority(), is(authority));
+ }
}
--- a/jetty-server/src/test/java/org/eclipse/jetty/server/handler/NcsaRequestLogTest.java
+++ b/jetty-server/src/test/java/org/eclipse/jetty/server/handler/NcsaRequestLogTest.java
@@ -358,9 +358,9 @@ public void testUseragentWithout(String logType) throws Exception
setup(logType);
testHandlerServerStart();
- _connector.getResponse("GET http://[:1]/foo HTTP/1.1\nReferer: http://other.site\n\n");
+ _connector.getResponse("GET http://[::1]/foo HTTP/1.1\nReferer: http://other.site\n\n");
String log = _entries.poll(5, TimeUnit.SECONDS);
- assertThat(log, containsString("GET http://[:1]/foo "));
+ assertThat(log, containsString("GET http://[::1]/foo "));
assertThat(log, containsString(" 400 50 \"http://other.site\" \"-\""));
}
@@ -371,9 +371,9 @@ public void testUseragentWith(String logType) throws Exception
setup(logType);
testHandlerServerStart();
- _connector.getResponse("GET http://[:1]/foo HTTP/1.1\nReferer: http://other.site\nUser-Agent: Mozilla/5.0 (test)\n\n");
+ _connector.getResponse("GET http://[::1]/foo HTTP/1.1\nReferer: http://other.site\nUser-Agent: Mozilla/5.0 (test)\n\n");
String log = _entries.poll(5, TimeUnit.SECONDS);
- assertThat(log, containsString("GET http://[:1]/foo "));
+ assertThat(log, containsString("GET http://[::1]/foo "));
assertThat(log, containsString(" 400 50 \"http://other.site\" \"Mozilla/5.0 (test)\""));
}
--- a/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java
+++ b/jetty-util/src/main/java/org/eclipse/jetty/util/URIUtil.java
@@ -18,6 +18,7 @@
package org.eclipse.jetty.util;
+import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
@@ -1345,6 +1346,74 @@ public static URI addPath(URI uri, String path)
return URI.create(buf.toString());
}
+ private static boolean isHexDigit(char c)
+ {
+ return (((c >= 'a') && (c <= 'f')) || // ALPHA (lower)
+ ((c >= 'A') && (c <= 'F')) || // ALPHA (upper)
+ ((c >= '0') && (c <= '9')));
+ }
+
+ /**
+ * Validate an IPv4 or IPv6 address.
+ * @param inetAddress the address to validate
+ * @throws IllegalArgumentException if the address is not valid
+ */
+ public static void validateInetAddress(String inetAddress)
+ {
+ try
+ {
+ InetAddress ignored = InetAddress.getByName(inetAddress);
+ }
+ catch (Throwable e)
+ {
+ throw new IllegalArgumentException("Bad [IPv6] address", e);
+ }
+ }
+
+ /**
+ * Validate and normalize the scheme,
+ *
+ * @param scheme The scheme to normalize
+ * @return The normalized version of the scheme
+ * @throws IllegalArgumentException If the scheme is not valid
+ */
+ public static String validateScheme(String scheme)
+ {
+ if (scheme == null || scheme.isEmpty())
+ throw new IllegalArgumentException("Bad scheme");
+
+ // scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
+ StringBuilder toLowerCase = null;
+ for (int i = 0; i < scheme.length(); i++)
+ {
+ char c = scheme.charAt(i);
+ if (c >= 'A' && c <= 'Z')
+ {
+ if (toLowerCase == null)
+ {
+ toLowerCase = new StringBuilder(scheme.length());
+ toLowerCase.append(scheme, 0, i);
+ }
+ toLowerCase.append(Character.toLowerCase(c));
+ }
+ else if (c >= 'a' && c <= 'z' ||
+ (i > 0 && (c >= '0' && c <= '9' ||
+ c == '.' ||
+ c == '+' ||
+ c == '-')))
+ {
+ if (toLowerCase != null)
+ toLowerCase.append(c);
+ }
+ else
+ {
+ throw new IllegalArgumentException("Bad scheme");
+ }
+ }
+
+ return toLowerCase == null ? scheme : toLowerCase.toString();
+ }
+
/**
* Combine two query strings into one. Each query string should not contain the beginning '?' character, but
* may contain multiple parameters separated by the '{@literal &}' character.