GHSA-w7x5-g22v-xqhr Eclipse Jetty: Path parameter traversal
Description (as reported)SummaryIn 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 VersionJetty: 12.1.8
JDK: 17.0.18
Maven: 3.9.14Maven 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.8Only confirmed Jetty 12.1.8 so far.Minimal ReproductionStarts a minimal Jetty server with the following security setup:The test then sends requests with no `Authorization` header.Observed result: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 CauseThe suspected root cause is in `URIUtil.canonicalPath()`.The relevant logic is approximately:For the input:`/public;/../admin/secret`when the outer loop reaches the semicolon: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: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 ConditionThe issue is not limited to a non-empty path parameter such as ";x".The more precise trigger shape is:`;[^/]*/.`Examples: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 DirectionA 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: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`.
CVSS v3.1 score: 5.3