Vulnerability GHSA-8gmh-2hhj-2hv9

Medium Risk
MEDIUM RISK
CVSS Score: 4.8
Score Range: 4.0–6.9
Medium severity vulnerabilities (CVSS 4.0–6.9). Important issues that meaningfully reduce security confidence.
17 hours ago
November 12, 2026 at 10:27 PM UTC
Addressable URI normalization resolves some dotted path segments to the wrong path
2.2.5 - 2.2.8 and 2.3.2 - 2.9.0
2.2.5 - 2.2.8 and 2.3.2 - 2.9.0

Summary

Addressable URI normalization resolves some dotted path segments to the wrong path

Details

Summary

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.

Details

normalize_path 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 RULE_2B_2C, then replaces every textual copy of that pair in the path with /:

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:

  • 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

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

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.

Impacted packages

Timeline

Published
17 hours ago
November 12, 2026 at 10:27 PM UTC
Fixed (2.9.1)
14 days ago
October 30, 2026 at 08:03 AM UTC
Last Modified
17 hours ago
November 12, 2026 at 10:45 PM UTC