GHSA-w7x5-g22v-xqhr is a medium-severity (CVSS 5.3) CWE-647 vulnerability in org.eclipse.jetty:jetty-util. A fix is available for org.eclipse.jetty:jetty-util — see the affected versions and patch details below.
Exploitation Status
No confirmed exploitation observed yet
- CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.
- CISA’s own triage has not observed active exploitation or public proof-of-concept code for this CVE as of its last assessment.
Exploitation and automatability from CISA’s SSVC triage for GHSA-w7x5-g22v-xqhr.
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
How urgent is this, really
GHSA-w7x5-g22v-xqhr by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.
Where this sits among everything scored
Of 394,803 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.
Real-World Exposure
org.eclipse.jetty:jetty-util☕org.eclipse.jetty:jetty-utilReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Maven packages — download data is not available via public APIs for these ecosystems.
Description
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:
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:
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:
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:
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.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| ☕Maven | org.eclipse.jetty:jetty-util | ≥ 12.0.0&&< 12.0.35 | 12.0.35org.eclipse.jetty:jetty-util:12.0.35 |
| ☕Maven | org.eclipse.jetty:jetty-util | ≥ 12.1.0&&< 12.1.9 | 12.1.9org.eclipse.jetty:jetty-util:12.1.9 |
Affected Products
jettyeclipseDetection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for org.eclipse.jetty:jetty-util, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update org.eclipse.jetty:jetty-util to 12.0.35 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-w7x5-g22v-xqhr is resolved across your whole dependency graph.
Workarounds
Resolve every user-supplied path to its canonical form and reject anything that escapes the intended directory, and run the component under an account that has no read or write access outside the directory it legitimately serves.
Fixing This On Your OS
If you run this on a Linux distribution, patch through your package manager against the distro's own security advisory below — it tracks the exact backported fix for your release, which can ship on a different timeline (and sometimes a different severity) than the upstream project.
Moderate impact: This path confusion vulnerability in Eclipse Jetty affects web applications that rely on Jetty to provide resolved paths. While Jetty itself correctly handles these paths and prevents direct file serving, vulnerable web applications may misinterpret unresolved paths, potentially leading to information…
Mitigation for this issue is either not available or the currently available options do not meet the Red Hat Product Security criteria comprising ease of use and deployment, applicability to widespread installation base, or stability.Source: Red Hat security advisory for GHSA-w7x5-g22v-xqhr (CC BY 4.0)
Frequently Asked Questions
Is GHSA-w7x5-g22v-xqhr in your dependencies?
Find it across Maven, including transitive dependencies.