---
id: "CVE-2026-126354"
page: "https://infrastack.ai/live/CVE-2026-126354"
kind: "vulnerability"
aliases: ["CVE-2026-126354", "GHSA-8gmh-2hhj-2hv9"]
title: "Addressable URI normalization resolves some dotted path segments to the wrong path"
severity: "medium"
cvss: 4.8
epss: null
exploited: false
status: "open"
category: "package"
products: [{"name": "addressable", "fixed": ["2.9.1"], "affected": ">= 2.2.5, < 2.9.1", "ecosystem": "rubygems"}]
ecosystems: ["rubygems"]
tags: ["addressable", "cwe-22", "cwe-436", "package", "ruby", "self-hosted", "web-app"]
not_affected_if: ["no-path-allowlist", "normalized-value-used-for-request", "rejects-dot-segments"]
published_at: "2026-11-12T22:19:20+00:00"
first_seen_at: "2026-11-12T22:31:52+00:00"
updated_at: "2026-11-12T22:40:38+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-8gmh-2hhj-2hv9", "first_seen_at": "2026-11-12T22:31:52+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-126354", "first_seen_at": "2026-11-12T22:40:38+00:00"}]
exploit_sources: []
---

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

Link for people: https://infrastack.ai/live/CVE-2026-126354 (this .md is for agents)

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.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- rubygems:addressable affected >= 2.2.5, < 2.9.1 fixed 2.9.1

## How to check

- Check the installed Addressable version with `bundle exec gem list addressable` or by inspecting `Gemfile.lock` for `addressable`.
- Look for code that calls `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.
- Confirm whether the original, unnormalized URI is later passed to an HTTP client, proxy, web server or browser.

## Timeline

- 2026-11-12T22:31:52+00:00 ghsa https://github.com/advisories/GHSA-8gmh-2hhj-2hv9
- 2026-11-12T22:40:38+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-126354

## Documents

- [advisory] Maintainer's advisory: api.github.com https://api.github.com/repos/sporkmonger/addressable/security-advisories/GHSA-8gmh-2hhj-2hv9
- [advisory] Maintainer's advisory: github.com https://github.com/sporkmonger/addressable/security/advisories/GHSA-8gmh-2hhj-2hv9
- [fix] Patch: github.com https://github.com/sporkmonger/addressable/commit/25c7c412a2f7e06c3cac92872e831f6d053703c5
- [fix] Release: github.com https://github.com/sporkmonger/addressable/releases/tag/addressable-2.9.1

## From the sources

### 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 `/`:

```ruby
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

```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.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.

---

_Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai_
