ADDRESSABLE · CVE-2026-126354 · Package
FIX → 2.9.1
Addressable's URI normalization resolves some paths with repeated `..` segments differently from RFC 3986, so `/../../public/x/../../secret` normalizes to `/public/x/secret` instead of `/secret`. An application that checks the Addressable-normalized path against an allowlist but sends the original URI to an RFC 3986 resolver can let a request reach a path outside the allowlist.
first public Oct 30, 2026 · 2 sources
Check it with your coding agent
It reads this page and tells you if your code or app is affected.
Applications that use the Ruby Addressable gem to normalize or join URLs, then make an access decision on the normalized path, and also send the original unnormalized URI to another component that resolves dot segments per RFC 3986. Applications that use Addressable's normalized value for both the check and the request, or that reject .. segments, are not affected.
Not affected if no-path-allowlist, normalized-value-used-for-request, rejects-dot-segments
Who cares addressable, ruby, self-hosted, web-app
bundle exec gem list addressable or by inspecting Gemfile.lock for addressable.Addressable::URI normalize, join, == or === on user-supplied URLs or paths and then uses the result for an access decision such as an allowlist or redirect check.Addressable::URI.normalize_path 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.
normalize_path in 2.9.0 () removes dot segments by repeating a set of rewrites until nothing changes. On each pass it finds the first pair with , then replaces every textual copy of that pair in the path with :
Are you affected?
Ask from here. InfraStack checks the affected versions against your stack and answers in your inbox.
InfraStack checks the affected versions against your stack and answers in Slack, with the evidence and the fix.
InfraStack checks it against your stack and answers on your report, with the evidence and the fix.
Upgrade to 2.9.1
lib/addressable/uri.rb/segment/../RULE_2B_2C/pair = normalized_path.match(RULE_2B_2C)
...
regexp = "/#{Regexp.escape(parent.to_s)}/\\.\\./|"
regexp += "(/#{Regexp.escape(current.to_s)}/\\.\\.$)"
...
mod ||= normalized_path.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:
.. segments, which can't go above the root, then removes x and public, giving /secret./../../ 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.
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.start_with?("/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
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:
/public/ may be proxied" or "only these redirect targets are allowed";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.
This page as .md · .json · CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N
GitHub Advisory Database, CC-BY 4.0 · NVD data is provided by NIST and is public domain. Posts quoted belong to their authors.