GHSA-w7x5-g22v-xqhr

Suggest an improvement
Source
https://github.com/advisories/GHSA-w7x5-g22v-xqhr
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-w7x5-g22v-xqhr/GHSA-w7x5-g22v-xqhr.json
JSON Data
https://api.test.osv.dev/v1/vulns/GHSA-w7x5-g22v-xqhr
Aliases
Downstream
CGA (138)
MINI (40)
Published
2026-07-22T22:57:36Z
Modified
2026-07-22T23:25:39Z
Severity
  • 5.3 (Medium) CVSS_V3 - CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N CVSS Calculator
Summary
Eclipse Jetty: Path parameter traversal
Details

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.

Database specific
{
    "cwe_ids": [
        "CWE-647"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-22T22:57:36Z",
    "nvd_published_at": "2026-07-14T09:16:42Z",
    "severity": "MODERATE"
}
References

Affected packages

Maven / org.eclipse.jetty:jetty-util

Package

Name
org.eclipse.jetty:jetty-util
View open source insights on deps.dev
Purl
pkg:maven/org.eclipse.jetty/jetty-util

Affected ranges

Type
ECOSYSTEM
Events
Introduced
12.0.0
Fixed
12.0.35

Affected versions

12.*
12.0.0
12.0.1
12.0.2
12.0.3
12.0.4
12.0.5
12.0.6
12.0.7
12.0.8
12.0.9
12.0.10
12.0.11
12.0.12
12.0.13
12.0.14
12.0.15
12.0.16
12.0.17
12.0.18
12.0.19
12.0.20
12.0.21
12.0.22
12.0.23
12.0.24
12.0.25
12.0.26
12.0.27
12.0.28
12.0.29
12.0.30
12.0.31
12.0.32
12.0.33
12.0.34

Database specific

last_known_affected_version_range
"<= 12.0.34"
source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-w7x5-g22v-xqhr/GHSA-w7x5-g22v-xqhr.json"

Maven / org.eclipse.jetty:jetty-util

Package

Name
org.eclipse.jetty:jetty-util
View open source insights on deps.dev
Purl
pkg:maven/org.eclipse.jetty/jetty-util

Affected ranges

Type
ECOSYSTEM
Events
Introduced
12.1.0
Fixed
12.1.9

Affected versions

12.*
12.1.0
12.1.1
12.1.2
12.1.3
12.1.4
12.1.5
12.1.6
12.1.7
12.1.8

Database specific

last_known_affected_version_range
"<= 12.1.8"
source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-w7x5-g22v-xqhr/GHSA-w7x5-g22v-xqhr.json"