{"id":"CVE-2026-125110","kind":"vulnerability","aliases":["CVE-2026-125110","GHSA-wvg4-6p58-3gh3"],"related":[],"title":"`Rack::Static` `header_rules` bypass via unnormalized dot segments in the request path.","description":"## Summary\r\n\r\n`Rack::Static` normalizes the request path with `Utils.clean_path_info` when deciding which file to serve, but matches `header_rules` against a path that is only URL-decoded. A request containing dot segments, raw or percent-encoded, is served the normalized target file while the header rules for that file's location are not applied.\r\n\r\nThis allows an attacker to receive a protected static file without the security headers configured for it.\r\n\r\n## Details\r\n\r\n`Rack::Static#call` computes the served path as `Utils.clean_path_info(Utils.unescape_path(path))`. `Rack::Static#applicable_rules` applies only `Utils.unescape_path` before testing string and regular expression rules. A request for `/assets/../uploads/file.html` or `/assets/..%2fuploads/file.html` is served as `uploads/file.html`, but the decoded path `/assets/../uploads/file.html` does not start with `uploads`, so a rule scoped to that directory never matches.\r\n\r\nThe fix for CVE-2026-34786 (GHSA-q4qf-9j86-f5mh) added URL decoding to rule matching but did not add dot segment normalization, so versions that include that fix remain affected.\r\n\r\n## Impact\r\n\r\nApplications that use `Rack::Static` `header_rules` to attach security-relevant headers to particular paths or file types are affected. An unauthenticated attacker who knows the path of a protected file can request it through a dot segment path and receive it without those headers, for example without `Content-Disposition: attachment` or a restrictive `Content-Security-Policy` that prevents uploaded HTML from rendering inline. The file itself is served unchanged. Applications that do not use path-specific `header_rules` are not affected.\r\n\r\n## Mitigation\r\n\r\n* Update to a patched version of Rack in which `header_rules` are matched against the same normalized path used to serve the file.\r\n* Reject requests containing dot segments at a reverse proxy or application boundary.\r\n* Do not rely solely on `Rack::Static` `header_rules` to prevent inline rendering of untrusted uploads; serve them from a separate origin.\r\n\r\n## Credit\r\n* Reporter and Remediation developer","summary":"Rack::Static matches header_rules against a path that is only URL-decoded, while serving the normalized file, so dot-segment requests skip rules. An unauthenticated attacker can fetch a protected file without the security headers configured for it, such as Content-Disposition or a restrictive Content-Security-Policy.","severity":"medium","cvss":5.3,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"rack","fixed":["2.2.25"],"affected":"< 2.2.25","ecosystem":"rubygems"},{"name":"rack","fixed":["3.1.23"],"affected":">= 3.0, < 3.1.23","ecosystem":"rubygems"},{"name":"rack","fixed":["3.2.8"],"affected":">= 3.2, < 3.2.8","ecosystem":"rubygems"}],"ecosystems":["rubygems"],"tags":["cwe-180","package","rack","rails","ruby","rubygems","self-hosted"],"not_affected_if":["managed-hosting","no-header-rules","static-export"],"how_to_check":"- Check the installed rack gem version: `bundle list rack` or look at `Gemfile.lock` for `rack`.\n- Search your app config for `Rack::Static` and `header_rules`; if none are used, the issue does not apply.\n- Check whether a reverse proxy already rejects dot segments in request paths.","fix":"- rack `< 2.2.25`: move to `2.2.25`\n- rack `>= 3.0, < 3.1.23`: move to `3.1.23`\n- rack `>= 3.2, < 3.2.8`: move to `3.2.8`","status":"open","published_at":"2026-10-30T02:18:56+00:00","first_seen_at":"2026-11-09T22:53:30+00:00","triaged_at":"2026-11-09T22:56:36+00:00","updated_at":"2026-11-09T22:56:36+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-11-09T22:56:36Z","model":"claude-haiku-5-5","output":{"fix":"- rack `< 2.2.25`: move to `2.2.25`\n- rack `>= 3.0, < 3.1.23`: move to `3.1.23`\n- rack `>= 3.2, < 3.2.8`: move to `3.2.8`","reach":"internet","title":"Rack: static file header rules bypassed by dot segments in request paths","impact":"low","summary":"Rack::Static matches header_rules against a path that is only URL-decoded, while serving the normalized file, so dot-segment requests skip rules. An unauthenticated attacker can fetch a protected file without the security headers configured for it, such as Content-Disposition or a restrictive Content-Security-Policy.","audience":"businesses","category":"package","nicknames":[],"who_cares":["rubygems","rack","ruby","rails","self-hosted"],"confidence":0.8,"fix_status":"available","needs_auth":"no","worst_case":"protected static files served without their configured security headers","analyst_note":"","how_to_check":"- Check the installed rack gem version: `bundle list rack` or look at `Gemfile.lock` for `rack`.\n- Search your app config for `Rack::Static` and `header_rules`; if none are used, the issue does not apply.\n- Check whether a reverse proxy already rejects dot segments in request paths.","plain_action":"","product_name":"","needs_analyst":false,"not_affected_if":["no-header-rules","static-export","managed-hosting"],"who_is_affected":"Applications that use Rack::Static with path-specific header_rules to attach security headers to particular files or file types, especially for serving untrusted uploads. Apps that do not use header_rules, or that serve no sensitive static files, are not exposed.","user_interaction":"no","product_ecosystem":"","needs_special_setup":"yes","severity_if_unknown":"medium"},"cost_usd":0.000838,"input_tokens":5290,"output_tokens":618,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-125110':16A,24A '-180':29B '-2026':15A,23A,230C '-34786':231C '/assets':201C '/assets/../uploads/file.html':199C,211C '2fuploads/file.html':202C '3gh3':21A '6p58':20A '9j86':235C 'add':237C,246C 'affect':257C,279C,344C 'allow':150C 'appli':148C,185C 'applic':183C,259C,332C,379C 'attach':267C,311C 'attack':66B,152C,282C 'boundari':380C 'bypass':5A 'call':169C 'comput':170C 'configur':76B,163C 'contain':123C,371C 'content':82B,88B,309C,316C 'content-disposit':81B,308C 'content-security-polici':87B,315C 'credit':403C 'cve':14A,22A,229C 'cwe':28B 'decid':103C 'decod':51B,120C,209C,239C 'detail':166C 'develop':407C 'directori':223C 'disposit':83B,310C 'dot':8A,59B,124C,247C,296C,372C 'dot-seg':58B 'encod':130C 'exampl':306C 'express':194C 'f5mh':236C 'fetch':68B 'file':56B,71B,105C,136C,143C,158C,276C,290C,327C,368C 'fix':227C,255C 'ghsa':18A,233C 'ghsa-q4qf-9j86-f5mh':232C 'ghsa-wvg4-6p58-3gh3':17A 'header':3A,41B,75B,110C,139C,162C,264C,271C,304C,340C,355C,388C 'host':37B 'html':322C 'impact':258C 'includ':253C 'info':101C,177C 'inlin':325C,392C 'know':284C 'locat':145C 'match':40B,109C,225C,242C,358C 'mitig':345C 'never':224C 'normal':55B,94C,134C,249C,362C 'origin':402C 'packag':30B 'particular':273C 'patch':349C 'path':13A,45B,97C,100C,114C,173C,176C,179C,180C,188C,210C,274C,286C,298C,338C,363C 'path-specif':337C 'percent':129C 'percent-encod':128C 'polici':90B,318C 'prevent':320C,391C 'protect':70B,156C,289C 'proxi':377C 'q4qf':234C 'rack':1A,25A,26A,27A,31B,38B,92C,167C,181C,262C,352C,386C 'rail':32B 'raw':126C 'receiv':154C,300C 'regular':193C 'reject':369C 'relev':270C 'reli':383C 'remain':256C 'remedi':406C 'render':324C,393C 'report':404C 'request':12A,61B,96C,122C,197C,292C,370C 'restrict':86B,314C 'revers':376C 'rubi':33B 'rubygem':34B 'rule':4A,42B,63B,111C,140C,184C,195C,219C,241C,265C,341C,356C,389C 'scope':220C 'secur':74B,89B,161C,269C,317C 'security-relev':268C 'segment':9A,60B,125C,248C,297C,373C 'self':36B 'self-host':35B 'separ':401C 'serv':53B,107C,132C,172C,204C,330C,366C,397C 'skip':62B 'sole':384C 'specif':339C 'start':214C 'static':2A,39B,93C,157C,168C,182C,263C,387C 'string':191C 'summari':91C 'target':135C 'test':190C 'type':277C 'unauthent':65B,281C 'unchang':331C 'unnorm':7A 'untrust':395C 'updat':346C 'upload':216C,321C,396C 'uploads/file.html':206C 'url':50B,119C,238C 'url-decod':49B,118C 'use':261C,336C,364C 'utils.clean':99C,175C 'utils.unescape':178C,187C 'version':251C,350C 'via':6A 'without':72B,159C,302C,307C 'wvg4':19A","nicknames":[],"mitigations":[],"who_affected":"Applications that use Rack::Static with path-specific header_rules to attach security headers to particular files or file types, especially for serving untrusted uploads. Apps that do not use header_rules, or that serve no sensitive static files, are not exposed.","tier":"C","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"businesses","plain_action":null,"pkeys":["rack"],"ptokens":["rack"],"labels":["cwe180","medium","package","rack","rails","ruby","rubygems","selfhosted"],"campaign_type":null,"search_a":"'-125110':16A,24A '-2026':15A,23A '3gh3':21A '6p58':20A 'bypass':5A 'cve':14A,22A 'dot':8A 'ghsa':18A 'ghsa-wvg4-6p58-3gh3':17A 'header':3A 'path':13A 'rack':1A,25A,26A,27A 'request':12A 'rule':4A 'segment':9A 'static':2A 'unnorm':7A 'via':6A 'wvg4':19A","search_b":"'-180':29B 'attack':66B 'configur':76B 'content':82B,88B 'content-disposit':81B 'content-security-polici':87B 'cwe':28B 'decod':51B 'disposit':83B 'dot':59B 'dot-seg':58B 'fetch':68B 'file':56B,71B 'header':41B,75B 'host':37B 'match':40B 'normal':55B 'packag':30B 'path':45B 'polici':90B 'protect':70B 'rack':31B,38B 'rail':32B 'request':61B 'restrict':86B 'rubi':33B 'rubygem':34B 'rule':42B,63B 'secur':74B,89B 'segment':60B 'self':36B 'self-host':35B 'serv':53B 'skip':62B 'static':39B 'unauthent':65B 'url':50B 'url-decod':49B 'without':72B","search_c":"'-2026':230C '-34786':231C '/assets':201C '/assets/../uploads/file.html':199C,211C '2fuploads/file.html':202C '9j86':235C 'add':237C,246C 'affect':257C,279C,344C 'allow':150C 'appli':148C,185C 'applic':183C,259C,332C,379C 'attach':267C,311C 'attack':152C,282C 'boundari':380C 'call':169C 'comput':170C 'configur':163C 'contain':123C,371C 'content':309C,316C 'content-disposit':308C 'content-security-polici':315C 'credit':403C 'cve':229C 'decid':103C 'decod':120C,209C,239C 'detail':166C 'develop':407C 'directori':223C 'disposit':310C 'dot':124C,247C,296C,372C 'encod':130C 'exampl':306C 'express':194C 'f5mh':236C 'file':105C,136C,143C,158C,276C,290C,327C,368C 'fix':227C,255C 'ghsa':233C 'ghsa-q4qf-9j86-f5mh':232C 'header':110C,139C,162C,264C,271C,304C,340C,355C,388C 'html':322C 'impact':258C 'includ':253C 'info':101C,177C 'inlin':325C,392C 'know':284C 'locat':145C 'match':109C,225C,242C,358C 'mitig':345C 'never':224C 'normal':94C,134C,249C,362C 'origin':402C 'particular':273C 'patch':349C 'path':97C,100C,114C,173C,176C,179C,180C,188C,210C,274C,286C,298C,338C,363C 'path-specif':337C 'percent':129C 'percent-encod':128C 'polici':318C 'prevent':320C,391C 'protect':156C,289C 'proxi':377C 'q4qf':234C 'rack':92C,167C,181C,262C,352C,386C 'raw':126C 'receiv':154C,300C 'regular':193C 'reject':369C 'relev':270C 'reli':383C 'remain':256C 'remedi':406C 'render':324C,393C 'report':404C 'request':96C,122C,197C,292C,370C 'restrict':314C 'revers':376C 'rule':111C,140C,184C,195C,219C,241C,265C,341C,356C,389C 'scope':220C 'secur':161C,269C,317C 'security-relev':268C 'segment':125C,248C,297C,373C 'separ':401C 'serv':107C,132C,172C,204C,330C,366C,397C 'sole':384C 'specif':339C 'start':214C 'static':93C,157C,168C,182C,263C,387C 'string':191C 'summari':91C 'target':135C 'test':190C 'type':277C 'unauthent':281C 'unchang':331C 'untrust':395C 'updat':346C 'upload':216C,321C,396C 'uploads/file.html':206C 'url':119C,238C 'url-decod':118C 'use':261C,336C,364C 'utils.clean':99C,175C 'utils.unescape':178C,187C 'version':251C,350C 'without':159C,302C,307C","cause_entry":null,"cause_path":null,"disclosures_checked_at":null,"first_public_at":"2026-10-30T02:18:56+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N","cvss_by":"security-advisories@github.com","poc_count":0,"sources":[{"source":"ghsa-repo","external_id":"GHSA-wvg4-6p58-3gh3","url":"https://github.com/rack/rack/security/advisories/GHSA-wvg4-6p58-3gh3","severity":"medium","published_at":"2026-10-30T02:18:56+00:00","modified_at":"2026-11-09T22:11:54+00:00","first_seen_at":"2026-11-09T22:53:30+00:00","fetched_at":"2026-11-09T23:08:32+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-125110","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-125110","severity":null,"published_at":"2026-11-09T22:18:42+00:00","modified_at":"2026-11-09T22:18:42+00:00","first_seen_at":"2026-11-10T00:28:47+00:00","fetched_at":"2026-11-10T00:28:47+00:00","note":null}],"documents":[{"id":110683,"url":"https://github.com/rack/rack/security/advisories/GHSA-wvg4-6p58-3gh3","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa-repo","publisher":"vendor","text":null,"published_at":"2026-10-30T02:18:56+00:00","relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"},{"id":112154,"url":"https://github.com/rack/rack/commit/18a07fe553afb4d8a678d6a948cd6ba1e11e28be","title":"Patch: github.com","kind":"other","source":"nvd","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Patch: github.com"},{"id":112155,"url":"https://github.com/rack/rack/commit/9d7f19484ffd5ed5e032678c26ee6eff96d3a0a3","title":"Patch: github.com","kind":"other","source":"nvd","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Patch: github.com"},{"id":112156,"url":"https://github.com/rack/rack/commit/e88097ccb88be44d06d47b42daffe04929516ee4","title":"Patch: github.com","kind":"other","source":"nvd","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Patch: 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"}