{"id":"CVE-2026-126351","kind":"vulnerability","aliases":["CVE-2026-126351","GHSA-qq38-m9mg-xjj5"],"related":[],"title":"Addressable: Denial of service in URI path normalization","description":"### Impact\n\n`Addressable::URI.normalize_path` takes time quadratic in the number of nested dot segments in a path (e.g., `\"/\" + \"a/\" * n + \"../\" * n`, which for n = 3 is `/a/a/a/../../../`), because it removes one dot segment per pass over the whole path. `URI#normalize`, `URI#normalized_path`, `URI#==`, `URI#===` and `URI#join` all call it. Normalizing, comparing or joining a URI taken from user input can result in uncontrolled resource consumption, leading to denial of service. On 2.9.0, normalizing a 34 KB path takes about 3.5 seconds on MRI Ruby 4.0, and the cost grows with the square of the path length.\n\nThis affects every Ruby implementation, including MRI Ruby 3.2 and later. `URI.parse` by itself is unaffected, as is code that never normalizes, compares, or joins URIs from untrusted sources.\n\n### Affected Versions\n\nThis vulnerability affects all versions of Addressable prior to 2.9.1. It is fixed in 2.9.1.\n\n### Workarounds\n\n- Limit the length of URIs taken from user input before calling `normalize`, `==` or `join` on them. Web servers often limit request lines to about 8 KB, which keeps the cost to about 150 milliseconds per call on typical hardware, but URIs from other sources (request bodies, headers such as `Location`, stored data) may not be limited.\n- Reject paths with many dot segments before normalizing them.\n\n### Credits\n\nReported by Matthew Mongeau (@halogenandtoast), Ruby Central Gem Security, who also suggested the single-pass approach used in the fix.\n\n### For more information\n\nIf there are any questions or comments about this advisory:\n* [Open an issue](https://github.com/sporkmonger/addressable/issues)","summary":"Addressable's path normalization removes dot segments one pass at a time, so cost grows with the square of the path length. A remote attacker who gets a crafted URI from user input into normalize, ==, or join can tie up CPU for seconds per call.","severity":"medium","cvss":5.9,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"addressable","fixed":["2.9.1"],"affected":"< 2.9.1","ecosystem":"rubygems"}],"ecosystems":["rubygems"],"tags":["addressable","cwe-407","package","ruby","rubygems","web-apps"],"not_affected_if":["no-untrusted-uris","patched-2.9.1","uri-parse-only"],"how_to_check":"- Check the installed version with `gem list addressable` or `bundle exec gem list addressable`, or look at `Gemfile.lock` for `addressable`.\n- Search your code for calls to `normalize`, `normalized_path`, `==`, `===` or `join` on URIs built from request input, headers or stored data.","fix":"- `addressable` `2.9.1` (affected: all versions before `2.9.1`)","status":"open","published_at":"2026-11-12T22:19:20+00:00","first_seen_at":"2026-11-12T22:36:52+00:00","triaged_at":"2026-11-12T22:38:24+00:00","updated_at":"2026-11-12T22:40:38+00:00","category":"package","confidence":0.85,"triage":{"at":"2026-11-12T22:38:24Z","model":"claude-haiku-5-5","output":{"fix":"- `addressable` `2.9.1` (affected: all versions before `2.9.1`)","reach":"internet","title":"Addressable: crafted URI paths cause quadratic-time slowdown and denial of service","impact":"low","summary":"Addressable's path normalization removes dot segments one pass at a time, so cost grows with the square of the path length. A remote attacker who gets a crafted URI from user input into normalize, ==, or join can tie up CPU for seconds per call.","audience":"developers","category":"package","nicknames":[],"who_cares":["ruby","rubygems","addressable","web-apps"],"confidence":0.85,"fix_status":"available","needs_auth":"no","worst_case":"Crafted URI input stalls request threads and exhausts CPU, causing denial of service.","analyst_note":"","how_to_check":"- Check the installed version with `gem list addressable` or `bundle exec gem list addressable`, or look at `Gemfile.lock` for `addressable`.\n- Search your code for calls to `normalize`, `normalized_path`, `==`, `===` or `join` on URIs built from request input, headers or stored data.","plain_action":"","product_name":"","needs_analyst":false,"not_affected_if":["uri-parse-only","no-untrusted-uris","patched-2.9.1"],"who_is_affected":"Applications that take URIs from untrusted sources (request parameters, request bodies, headers such as Location, or stored data) and then normalize, compare or join them with Addressable. Code that only calls URI.parse, or never normalizes, compares or joins untrusted URIs, is not exposed.","user_interaction":"no","product_ecosystem":"","needs_special_setup":"no","severity_if_unknown":"medium"},"cost_usd":0.000807,"input_tokens":5162,"output_tokens":582,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-126351':11A,19A '-2026':10A,18A '-407':23B '/a/a/a/../../..':101C '/sporkmonger/addressable/issues)':326C '150':253C '2.9.0':149C '2.9.1':214C,219C '3':99C '3.2':182C '3.5':157C '34':152C '4.0':162C '8':245C 'address':1A,21B,30B,76C,211C 'addressable':20A 'advisori':320C 'affect':175C,203C,207C 'also':297C 'app':29B 'approach':303C 'attack':54B 'bodi':266C 'call':74B,125C,231C,256C 'central':293C 'code':192C 'comment':317C 'compar':128C,196C 'consumpt':142C 'cost':43B,165C,250C 'cpu':70B 'craft':58B 'credit':286C 'cve':9A,17A 'cwe':22B 'data':272C 'denial':2A,145C 'dot':35B,87C,106C,281C 'e.g':92C 'everi':176C 'fix':217C,307C 'gem':294C 'get':56B 'ghsa':13A 'ghsa-qq38-m9mg-xjj5':12A 'github.com':325C 'github.com/sporkmonger/addressable/issues)':324C 'grow':44B,166C 'halogenandtoast':291C 'hardwar':259C 'header':267C 'impact':75C 'implement':178C 'includ':179C 'inform':310C 'input':62B,136C,229C 'issu':323C 'join':66B,123C,130C,198C,234C 'kb':153C,246C 'keep':248C 'later':184C 'lead':143C 'length':51B,173C,223C 'limit':221C,240C,276C 'line':242C 'locat':270C 'm9mg':15A 'mani':280C 'matthew':289C 'may':273C 'millisecond':254C 'mongeau':290C 'mri':160C,180C 'n':94C,95C,98C 'nest':86C 'never':194C 'normal':8A,33B,64B,115C,117C,127C,150C,195C,232C,284C 'number':84C 'often':239C 'one':37B,105C 'open':321C 'packag':24B 'pass':38B,109C,302C 'path':7A,32B,50B,78C,91C,113C,118C,154C,172C,278C 'per':73B,108C,255C 'prior':212C 'qq38':14A 'quadrat':81C 'question':315C 'reject':277C 'remot':53B 'remov':34B,104C 'report':287C 'request':241C,265C 'resourc':141C 'result':138C 'rubi':25B,161C,177C,181C,292C 'rubygem':26B 'second':72B,158C 'secur':295C 'segment':36B,88C,107C,282C 'server':238C 'servic':4A,147C 'singl':301C 'single-pass':300C 'sourc':202C,264C 'squar':47B,169C 'store':271C 'suggest':298C 'take':79C,155C 'taken':133C,226C 'tie':68B 'time':41B,80C 'typic':258C 'unaffect':189C 'uncontrol':140C 'untrust':201C 'uri':6A,59B,114C,116C,119C,120C,122C,132C,199C,225C,261C 'uri.normalize':77C 'uri.parse':185C 'use':304C 'user':61B,135C,228C 'version':204C,209C 'vulner':206C 'web':28B,237C 'web-app':27B 'whole':112C 'workaround':220C 'xjj5':16A","nicknames":[],"mitigations":[],"who_affected":"Applications that take URIs from untrusted sources (request parameters, request bodies, headers such as Location, or stored data) and then normalize, compare or join them with Addressable. Code that only calls URI.parse, or never normalizes, compares or joins untrusted URIs, is not exposed.","tier":"C","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"developers","plain_action":null,"pkeys":["addressable"],"ptokens":["addressable"],"labels":["addressable","cwe407","medium","package","ruby","rubygems","webapps"],"campaign_type":null,"search_a":"'-126351':11A,19A '-2026':10A,18A 'address':1A 'addressable':20A 'cve':9A,17A 'denial':2A 'ghsa':13A 'ghsa-qq38-m9mg-xjj5':12A 'm9mg':15A 'normal':8A 'path':7A 'qq38':14A 'servic':4A 'uri':6A 'xjj5':16A","search_b":"'-407':23B 'address':21B,30B 'app':29B 'attack':54B 'call':74B 'cost':43B 'cpu':70B 'craft':58B 'cwe':22B 'dot':35B 'get':56B 'grow':44B 'input':62B 'join':66B 'length':51B 'normal':33B,64B 'one':37B 'packag':24B 'pass':38B 'path':32B,50B 'per':73B 'remot':53B 'remov':34B 'rubi':25B 'rubygem':26B 'second':72B 'segment':36B 'squar':47B 'tie':68B 'time':41B 'uri':59B 'user':61B 'web':28B 'web-app':27B","search_c":"'/a/a/a/../../..':101C '/sporkmonger/addressable/issues)':326C '150':253C '2.9.0':149C '2.9.1':214C,219C '3':99C '3.2':182C '3.5':157C '34':152C '4.0':162C '8':245C 'address':76C,211C 'advisori':320C 'affect':175C,203C,207C 'also':297C 'approach':303C 'bodi':266C 'call':125C,231C,256C 'central':293C 'code':192C 'comment':317C 'compar':128C,196C 'consumpt':142C 'cost':165C,250C 'credit':286C 'data':272C 'denial':145C 'dot':87C,106C,281C 'e.g':92C 'everi':176C 'fix':217C,307C 'gem':294C 'github.com':325C 'github.com/sporkmonger/addressable/issues)':324C 'grow':166C 'halogenandtoast':291C 'hardwar':259C 'header':267C 'impact':75C 'implement':178C 'includ':179C 'inform':310C 'input':136C,229C 'issu':323C 'join':123C,130C,198C,234C 'kb':153C,246C 'keep':248C 'later':184C 'lead':143C 'length':173C,223C 'limit':221C,240C,276C 'line':242C 'locat':270C 'mani':280C 'matthew':289C 'may':273C 'millisecond':254C 'mongeau':290C 'mri':160C,180C 'n':94C,95C,98C 'nest':86C 'never':194C 'normal':115C,117C,127C,150C,195C,232C,284C 'number':84C 'often':239C 'one':105C 'open':321C 'pass':109C,302C 'path':78C,91C,113C,118C,154C,172C,278C 'per':108C,255C 'prior':212C 'quadrat':81C 'question':315C 'reject':277C 'remov':104C 'report':287C 'request':241C,265C 'resourc':141C 'result':138C 'rubi':161C,177C,181C,292C 'second':158C 'secur':295C 'segment':88C,107C,282C 'server':238C 'servic':147C 'singl':301C 'single-pass':300C 'sourc':202C,264C 'squar':169C 'store':271C 'suggest':298C 'take':79C,155C 'taken':133C,226C 'time':80C 'typic':258C 'unaffect':189C 'uncontrol':140C 'untrust':201C 'uri':114C,116C,119C,120C,122C,132C,199C,225C,261C 'uri.normalize':77C 'uri.parse':185C 'use':304C 'user':135C,228C 'version':204C,209C 'vulner':206C 'web':237C 'whole':112C 'workaround':220C","cause_entry":null,"cause_path":null,"disclosures_checked_at":null,"first_public_at":"2026-10-30T08:13:55+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-qq38-m9mg-xjj5","url":"https://github.com/advisories/GHSA-qq38-m9mg-xjj5","severity":"medium","published_at":"2026-11-12T22:35:33+00:00","modified_at":"2026-11-12T22:35:34+00:00","first_seen_at":"2026-11-12T22:36:52+00:00","fetched_at":"2026-11-12T22:42:03+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-126351","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-126351","severity":null,"published_at":"2026-11-12T22:19:20+00:00","modified_at":"2026-11-12T22:19:20+00:00","first_seen_at":"2026-11-12T22:40:38+00:00","fetched_at":"2026-11-12T22:40:38+00:00","note":null}],"documents":[{"id":139750,"url":"https://api.github.com/repos/sporkmonger/addressable/security-advisories/GHSA-qq38-m9mg-xjj5","title":"Maintainer's advisory: api.github.com","kind":"vendor","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"advisory","confidence":null,"note":"Maintainer's advisory: api.github.com"},{"id":139751,"url":"https://github.com/sporkmonger/addressable/security/advisories/GHSA-qq38-m9mg-xjj5","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa","publisher":"vendor","text":null,"published_at":"2026-10-30T08:13:55+00:00","relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"},{"id":139744,"url":"https://github.com/sporkmonger/addressable/commit/25c7c412a2f7e06c3cac92872e831f6d053703c5","title":"Patch: github.com","kind":"other","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Patch: github.com"},{"id":139737,"url":"https://github.com/sporkmonger/addressable/releases/tag/addressable-2.9.1","title":"Release: github.com","kind":"release","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Release: github.com"}],"disclosures":[],"exploit_sources":[],"notice":"Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai"}