1 line
5.7 KiB
JSON
1 line
5.7 KiB
JSON
{"schema_version":"1.9.2","id":"GHSA-8gmh-2hhj-2hv9","published":"2026-11-12T22:27:58Z","modified":"2026-11-12T22:45:04.162582601Z","aliases":["CVE-2026-126354"],"summary":"Addressable URI normalization resolves some dotted path segments to the wrong path","details":"### Summary\n\n`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.\n\n### Details\n\n`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 `/`:\n\n```ruby\npair = normalized_path.match(RULE_2B_2C)\n...\nregexp = \"/#{Regexp.escape(parent.to_s)}/\\\\.\\\\./|\"\nregexp += \"(/#{Regexp.escape(current.to_s)}/\\\\.\\\\.$)\"\n...\nmod ||= normalized_path.gsub!(Regexp.new(regexp), SLASH)\n```\n\nWhen 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`:\n\n- RFC 3986 §5.2.4 drops the two leading `..` segments, which can't go above the root, then removes `x` and `public`, giving `/secret`.\n- 2.9.0 replaces both copies of `/../../` with `/` in one step, giving `/public/x/secret`.\n\nPaths where an ordinary `/segment/../` pair comes first, such as `/a/../../b/c/../../d`, are resolved correctly.\n\nThis 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.\n\n### Proof of concept\n\n```ruby\nrequire \"addressable/uri\"\nrequire \"uri\"\n\nurl = \"http://example.com/../../public/x/../../secret\"\n\n# Addressable's normalized path, compared with RFC 3986 resolution by Ruby's URI library\nputs Addressable::URI.parse(url).normalize.path\nputs URI.join(\"http://example.com/\", \"../../public/x/../../secret\").path\n\n# join and == use the same normalization\nputs Addressable::URI.join(\"http://example.com/\", \"../../public/x/../../secret\").path\nputs Addressable::URI.parse(url) == Addressable::URI.parse(\"http://example.com/public/x/secret\")\n\n# An allowlist check on the normalized path passes, but the request reaches /secret\ndef allowed?(url) = Addressable::URI.parse(url).normalize.path.start_with?(\"/public/\")\nputs allowed?(url)\n```\n\nOutput with Addressable 2.9.0:\n\n```\n/public/x/secret\n/secret\n/public/x/secret\ntrue\ntrue\n```\n\nOutput with Addressable 2.9.1:\n\n```\n/secret\n/secret\n/secret\nfalse\nfalse\n```\n\n\n### Impact\n\nThis 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:\n\n- 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\";\n- 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.\n\nAn 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.\n\nAs 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.","affected":[{"package":{"name":"addressable","ecosystem":"RubyGems","purl":"pkg:gem/addressable"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"2.2.5"},{"fixed":"2.9.1"}]}],"versions":["2.2.5","2.2.6","2.2.7","2.2.8","2.3.2","2.3.3","2.3.4","2.3.5","2.3.6","2.3.7","2.3.8","2.4.0","2.5.0","2.5.1","2.5.2","2.6.0","2.7.0","2.8.0","2.8.1","2.8.10","2.8.2","2.8.3","2.8.4","2.8.5","2.8.6","2.8.7","2.8.8","2.8.9","2.9.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/11/GHSA-8gmh-2hhj-2hv9/GHSA-8gmh-2hhj-2hv9.json"}}],"references":[{"type":"WEB","url":"https://github.com/sporkmonger/addressable/security/advisories/GHSA-8gmh-2hhj-2hv9"},{"type":"WEB","url":"https://github.com/sporkmonger/addressable/commit/25c7c412a2f7e06c3cac92872e831f6d053703c5"},{"type":"WEB","url":"https://github.com/rubysec/ruby-advisory-db/blob/master/gems/addressable/GHSA-8gmh-2hhj-2hv9.yml"},{"type":"PACKAGE","url":"https://github.com/sporkmonger/addressable"},{"type":"WEB","url":"https://github.com/sporkmonger/addressable/releases/tag/addressable-2.9.1"}],"database_specific":{"cwe_ids":["CWE-22","CWE-436"],"github_reviewed":true,"github_reviewed_at":"2026-11-12T22:27:58Z","nvd_published_at":null,"severity":"MODERATE"},"severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N"}]} |