GHSA-W7X5-G22V-XQHR

Vulnerability from github – Published: 2026-07-22 22:57 – Updated: 2026-07-22 22:57
VLAI
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.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 12.0.34"
      },
      "package": {
        "ecosystem": "Maven",
        "name": "org.eclipse.jetty:jetty-util"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "12.0.0"
            },
            {
              "fixed": "12.0.35"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 12.1.8"
      },
      "package": {
        "ecosystem": "Maven",
        "name": "org.eclipse.jetty:jetty-util"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "12.1.0"
            },
            {
              "fixed": "12.1.9"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-8384"
  ],
  "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"
  },
  "details": "### Description (as reported)\n\n#### Summary\n\nIn 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\n  segment.\n\nA minimal example is:\n\n`/public;/../admin/secret`\n\nIn my local reproduction, URIUtil.canonicalPath() returns:\n\n`/public/../admin/secret`\n\ninstead of the expected normalized path:\n\n`/admin/secret`\n\nWhen Jetty\u0027s `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.\n\n\n\n#### Tested Version\n\nJetty: 12.1.8\nJDK: 17.0.18\nMaven: 3.9.14\n\nMaven artifacts used:\n\n  org.eclipse.jetty:jetty-server:12.1.8\n  org.eclipse.jetty:jetty-security:12.1.8\n  org.eclipse.jetty:jetty-session:12.1.8\n\nOnly confirmed Jetty 12.1.8 so far. \n\n\n#### Minimal Reproduction\n\nStarts a minimal Jetty server with the following security setup:\n\n```java\nSecurityHandler.PathMapped security = new SecurityHandler.PathMapped();\nsecurity.put(\"/admin/*\", Constraint.from(\"admin\"));\nsecurity.put(\"/*\", Constraint.ALLOWED);\nsecurity.setAuthenticator(new BasicAuthenticator());\n```\n\nThe test then sends requests with no `Authorization` header.\n\nObserved result:\n\n```\nGET /admin/secret                  -\u003e 401\nGET /public;x/../admin/secret      -\u003e 200\n```\n\nThe handler receives paths such as:\n\n`/public/../admin/secret`\n\nThis suggests that the `/admin/*` security constraint is bypassed because `PathMapped` matching is performed against the non-normalized canonical path.\n\n\n#### Suspected Root Cause\n\nThe suspected root cause is in `URIUtil.canonicalPath()`.\n\nThe relevant logic is approximately:\n\n```java\n    for (int i = 0; i \u003c end; i++)\n    {\n        char c = encodedPath.charAt(i);\n\n        switch (c)\n        {\n            case \u0027;\u0027:\n                if (builder == null)\n                {\n                    builder = new Utf8StringBuilder(encodedPath.length());\n                    builder.append(encodedPath, 0, i);\n                }\n\n                while (++i \u003c end)\n                {\n                    if (encodedPath.charAt(i) == \u0027/\u0027)\n                    {\n                        builder.append(\u0027/\u0027);\n                        break;\n                    }\n                }\n                break;\n\n            case \u0027.\u0027:\n                if (slash)\n                    normal = false;\n                if (builder != null)\n                    builder.append(c);\n                break;\n        }\n\n        slash = c == \u0027/\u0027;\n    }\n\n    String canonical = (builder != null)\n        ? (onBadUtf8 == null ? builder.toCompleteString() : builder.takeCompleteString(onBadUtf8))\n        : encodedPath;\n    return normal ? canonical : normalizePath(canonical);\n```\n\nFor the input:\n\n`/public;/../admin/secret`\n\nwhen the outer loop reaches the semicolon:\n\n```\n    i      = 7\n    c      = \u0027;\u0027\n    slash  = false\n    normal = true\n```\n\nInside `case \u0027;\u0027`, the `while (++i \u003c end)` loop advances i to the next character, which is already \u0027/\u0027 for the empty path parameter form \";/\".\n\nThe code then appends \u0027/\u0027 to the canonical builder:\n\n`builder.append(\u0027/\u0027);`\n\nAt this point, the canonical builder ends with \u0027/\u0027:\n\n`/public/`\n\nHowever, the local variable `c` is still the old value \u0027;\u0027, because `c` was read before entering the switch and is not updated when the inner loop advances `i`.\n\nAfter leaving the switch, the loop updates the slash state using:\n\n`slash = c == \u0027/\u0027;`\n\nSince `c` is still \u0027;\u0027, slash becomes `false`.\n\nOn the next iteration, the scanner reaches \u0027.\u0027, which is the first dot in the following \"../\" segment. Because slash is incorrectly `false`, this code does not run:\n\n```java\n    if (slash)\n        normal = false;\n```\n\nTherefore `normal` remains `true`, and `canonicalPath()` returns the canonical string directly instead of calling `normalizePath(canonical)`.\n\nThe result is:\n\n`/public/../admin/secret`\n\ninstead of:\n\n`/admin/secret`\n\nIn short:\n\n`case \u0027;\u0027` advances the scan position i and appends \u0027/\u0027 to the canonical builder, but the loop tail still updates slash from the stale character `c=\u0027;\u0027`. As a result, the following dot-dot segment is not detected as a path traversal segment.\n\n####  More Precise Trigger Condition\n\nThe issue is not limited to a non-empty path parameter such as \";x\".\n\nThe more precise trigger shape is:\n\n`;[^/]*/.`\n\nExamples:\n\n```\n    /public;/../admin/secret\n    /public;x/../admin/secret\n    /public;anything/../admin/secret\n    /public;/./admin/secret\n```\n\nThe minimal form is:\n\n`/public;/../admin/secret`\n\nbecause the semicolon is immediately followed by \u0027/\u0027, so the inner while loop reaches \u0027/\u0027 on its first increment.\n\n####  Potential Minimal Fix Direction\n\nA minimal fix would be to ensure that, when case \u0027;\u0027 consumes input until \u0027/\u0027 and appends \u0027/\u0027 to the canonical builder, the slash state reflects the last effective character in the canonical path.\n\nFor example, conceptually:\n\n```java\n    case \u0027;\u0027:\n        if (builder == null)\n        {\n            builder = new Utf8StringBuilder(encodedPath.length());\n            builder.append(encodedPath, 0, i);\n        }\n\n        while (++i \u003c end)\n        {\n            if (encodedPath.charAt(i) == \u0027/\u0027)\n            {\n                builder.append(\u0027/\u0027);\n                slash = true;\n                break;\n            }\n        }\n        continue;\n```\n\nThe important part is to avoid the loop tail from overwriting slash using the stale `c` value:\n\n`slash = c == \u0027/\u0027;`\n\nIn other words, `slash` should represent the last effective character appended to the canonical builder, not the original input character read before case \u0027;\u0027 advanced `i`.",
  "id": "GHSA-w7x5-g22v-xqhr",
  "modified": "2026-07-22T22:57:36Z",
  "published": "2026-07-22T22:57:36Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/jetty/jetty.project/security/advisories/GHSA-w7x5-g22v-xqhr"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-8384"
    },
    {
      "type": "WEB",
      "url": "https://github.com/jetty/jetty.project/pull/14969"
    },
    {
      "type": "WEB",
      "url": "https://github.com/jetty/jetty.project/pull/14973"
    },
    {
      "type": "WEB",
      "url": "https://github.com/jetty/jetty.project/commit/82969c77f6da46e27008b10b3c14840cd31db084"
    },
    {
      "type": "WEB",
      "url": "https://github.com/jetty/jetty.project/commit/ade27ce93a37c33278720250d85c48601230ae3f"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/jetty/jetty.project"
    },
    {
      "type": "WEB",
      "url": "https://github.com/jetty/jetty.project/releases/tag/jetty-12.0.35"
    },
    {
      "type": "WEB",
      "url": "https://github.com/jetty/jetty.project/releases/tag/jetty-12.1.9"
    },
    {
      "type": "WEB",
      "url": "https://gitlab.eclipse.org/security/cve-assignment/-/work_items/108"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Eclipse Jetty: Path parameter traversal"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Loading…