CVE-2026-126354: Addressable URI normalization resolves some dotted path segments to the wrong path
Summary
Addressable::URI.normalizepath resolves some paths containing .. segments to a different path than RFC 3986 does. When a path climbs above the root before any other .. is resolved, the segments that later .. segments should remove are instead kept: /../../public/x/../../secret normalizes to /public/x/secret instead of /secret. URI#normalize, URI#join, URI#== and URI#=== all use this normalization. An application that checks a normalized Addressable path against an allowlist, but sends the original URI to something that resolves it according to RFC 3986, can be made to allow a request for a path outside the allowlist.
Details
normalizepath in 2.9.0 (lib/addressable/uri.rb) removes dot segments by repeating a set of rewrites until nothing changes. On each pass it finds the first /segment/../ pair with RULE2B2C, then replaces every textual copy of that pair in the path with /:
ruby pair = normalizedpath.match(RULE2B2C) ... regexp = "/#{Regexp.escape(parent.tos)}/\\.\\./|" regexp += "(/#{Regexp.escape(current.tos)}/\\.\\.$)" ... mod ||= normalizedpath.gsub!(Regexp.new(regexp), SLASH)
When the first pair found is /../../, where the "segment" is itself .., every /../../ in the path is replaced with /. That includes copies later in the path, where each .. should have removed the segment before it. For /../../public/x/../../secret:
- RFC 3986 §5.2.4 drops the two leading .. segments, which can't go above the root, then removes x and public, giving /secret. - 2.9.0 replaces both copies of /../../ with / in one step, giving /public/x/secret.
Paths where an ordinary /segment/../ pair comes first, such as /a/../../b/c/../../d, are resolved correctly.
This was introduced in 2.2.5. Versions 2.0.0 through 2.2.4 and earlier versions resolve these paths correctly. It is fixed in 2.9.1, which replaces the rewrite loop with a single pass that correctly follows RFC 3986 §5.2.4.
Proof of concept
ruby require "addressable/uri" require "uri"
url = "http://example.com/../../public/x/../../secret"
Addressable's normalized path, compared with RFC 3986 resolution by Ruby's URI library puts Addressable::URI.parse(url).normalize.path puts URI.join("http://example.com/", "../../public/x/../../secret").path
join and == use the same normalization puts Addressable::URI.join("http://example.com/", "../../public/x/../../secret").path puts Addressable::URI.parse(url) == Addressable::URI.parse("http://example.com/public/x/secret")
An allowlist check on the normalized path passes, but the request reaches /secret def allowed?(url) = Addressable::URI.parse(url).normalize.path.startwith?("/public/") puts allowed?(url)
Output with Addressable 2.9.0:
/public/x/secret /secret /public/x/secret true true
Output with Addressable 2.9.1:
/secret /secret /secret false false
Impact
This is an interpretation conflict (CWE-436) between Addressable and other RFC 3986 resolvers, which could lead to path traversal past an allowlist (CWE-22). An application is affected when it does both of these:
- makes an access decision on a path normalized or joined by Addressable, such as "only paths under /public/ may be proxied" or "only these redirect targets are allowed"; - sends the original, unnormalized URI to a component that resolves dot segments itself, such as an HTTP client, a proxy, a web server or a browser.
An attacker may be able to bypass a check by exploiting behavioral differences between URI parsers. Impact from this is application-dependent. Applications that use Addressable's normalized form for both the check and the request are unaffected, as are applications that reject paths containing .. segments.
As a work-around for unpatched versions, before making access decisions on a path, either reject any containing .. segments, including percent-encoded forms such as %2e%2e, or use the same normalized value for both the check and the request.
Other sources
Addressable is an alternative implementation to the URI implementation that is part of Ruby's standard library. From 2.2.5 until 2.9.1, Addressable::URI.normalizepath can resolve a path containing leading parent segments and later parent segments to a different path than an RFC 3986 resolver. URI#normalize, URI#join, URI#==, and URI#=== inherit the result. An application that authorizes the Addressable-normalized path but sends the original URI to another resolver can allow access outside the intended path allowlist. Applications that use the same normalized value for authorization and the request, or reject parent segments, are unaffected. This issue is fixed in version 2.9.1.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
rubygems/addressableto a version that resolves this vulnerability.Fixed in 2.9.1 - Upgrade
Upgrade
Addressableto a version that resolves this vulnerability.Fixed in 2.9.1 - Compensating control
For unpatched versions, before making access decisions on a path, either reject paths containing '..' segments, including percent-encoded forms such as '%2e%2e', or use the same normalized value for both authorization and the request.
Event History
Frequently Asked Questions
What application designs are exposed to this issue?
Applications are exposed when they check an Addressable-normalized path against an allowlist but send the original URI to a downstream component that resolves dot segments according to RFC 3986. This discrepancy can cause a request outside the allowlisted path to be accepted.
What does an attacker need to exploit it?
An attacker needs to provide a URI path containing dot-dot segments arranged so that the path climbs above the root before later dot-dot segments are processed. No privileges or user interaction are indicated by the provided vector.
How can I determine whether my application is affected?
Review uses of Addressable::URI.normalize_path, URI#normalize, URI#join, URI#==, and URI#=== in authorization or allowlist decisions. Test whether a path such as /../../public/x/../../secret is approved after Addressable normalization while the receiving component resolves it to /secret under RFC 3986 rules.
What can be done if an immediate upgrade is not possible?
Do not make allowlist or authorization decisions using Addressable-normalized paths when the original URI will be resolved by another component. Ensure the component enforcing the allowlist and the component handling the request apply the same path-normalization behavior.