{"affected":[{"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"},"package":{"ecosystem":"Maven","name":"org.eclipse.jetty:jetty-util","purl":"pkg:maven/org.eclipse.jetty/jetty-util"},"ranges":[{"events":[{"introduced":"12.0.0"},{"fixed":"12.0.35"}],"type":"ECOSYSTEM"}],"versions":["12.0.0","12.0.1","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.2","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.3","12.0.30","12.0.31","12.0.32","12.0.33","12.0.34","12.0.4","12.0.5","12.0.6","12.0.7","12.0.8","12.0.9"]},{"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"},"package":{"ecosystem":"Maven","name":"org.eclipse.jetty:jetty-util","purl":"pkg:maven/org.eclipse.jetty/jetty-util"},"ranges":[{"events":[{"introduced":"12.1.0"},{"fixed":"12.1.9"}],"type":"ECOSYSTEM"}],"versions":["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"]}],"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'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.\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                  -> 401\nGET /public;x/../admin/secret      -> 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 < end; i++)\n    {\n        char c = encodedPath.charAt(i);\n\n        switch (c)\n        {\n            case ';':\n                if (builder == null)\n                {\n                    builder = new Utf8StringBuilder(encodedPath.length());\n                    builder.append(encodedPath, 0, i);\n                }\n\n                while (++i < end)\n                {\n                    if (encodedPath.charAt(i) == '/')\n                    {\n                        builder.append('/');\n                        break;\n                    }\n                }\n                break;\n\n            case '.':\n                if (slash)\n                    normal = false;\n                if (builder != null)\n                    builder.append(c);\n                break;\n        }\n\n        slash = c == '/';\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      = ';'\n    slash  = false\n    normal = true\n```\n\nInside `case ';'`, the `while (++i < end)` loop advances i to the next character, which is already '/' for the empty path parameter form \";/\".\n\nThe code then appends '/' to the canonical builder:\n\n`builder.append('/');`\n\nAt this point, the canonical builder ends with '/':\n\n`/public/`\n\nHowever, 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`.\n\nAfter leaving the switch, the loop updates the slash state using:\n\n`slash = c == '/';`\n\nSince `c` is still ';', slash becomes `false`.\n\nOn 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:\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 ';'` 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.\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 '/', so the inner while loop reaches '/' on its first increment.\n\n####  Potential Minimal Fix Direction\n\nA 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.\n\nFor example, conceptually:\n\n```java\n    case ';':\n        if (builder == null)\n        {\n            builder = new Utf8StringBuilder(encodedPath.length());\n            builder.append(encodedPath, 0, i);\n        }\n\n        while (++i < end)\n        {\n            if (encodedPath.charAt(i) == '/')\n            {\n                builder.append('/');\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 == '/';`\n\nIn other words, `slash` should represent the last effective character appended to the canonical builder, not the original input character read before case ';' advanced `i`.","id":"GHSA-w7x5-g22v-xqhr","modified":"2026-09-10T03:51:12.485862890Z","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.9.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"}