{"id":"CVE-2026-128069","kind":"vulnerability","aliases":["CVE-2026-128069","GHSA-h7wh-q2qr-458c"],"related":[],"title":"rails-html-sanitizer: Possible XSS via shared scrubber state in concurrent calls","description":"## Summary\n\n`Rails::HTML::SafeListSanitizer` (both the HTML4 and HTML5 variants) shares one `Rails::HTML::PermitScrubber` across every `sanitize` call on the same instance, assigning its `tags` and `attributes` on each call without synchronization. Two concurrent `sanitize` calls with different `tags:` or `attributes:` options can interleave, so one call is scrubbed with the other call's allowlist.\n\n- Versions affected: `< 1.7.2`\n- Not affected: none\n- Fixed versions: `1.7.2`\n\n## Impact\n\nAction View memoizes a single `SafeListSanitizer` per helper module and uses it for every `sanitize` call in the process, so in a multi-threaded server (for example Puma) all requests share the same `PermitScrubber`. If any code path calls `sanitize` with a more permissive allowlist, an attacker who can trigger that path concurrently with another request can cause the other request to be sanitized with the permissive allowlist. If the permissive allowlist admits `script` or an event-handler attribute, the strict call emits it unchanged, which is a cross-site scripting vector when the strict output is stored or rendered to other users.\n\nApplications are affected only if they run more than one thread per process and some `sanitize` call passes `tags:` or `attributes:` options that differ from another call's, including a call that passes no options and so uses the defaults. Applications that use a single configuration everywhere, pass their own `scrubber:`, or run single-threaded are not affected.\n\n## Workarounds\n\nUse the same allowlist for every `sanitize` call, or pass a `scrubber:` built per call (`Rails::HTML::PermitScrubber.new` with `tags=` and `attributes=` set) instead of `tags:` and `attributes:` options.\n\n## Credit\n\nReported by hiumee via HackerOne.","summary":"rails-html-sanitizer shares one scrubber across sanitize calls in a process, so concurrent calls with different allowlists can interleave. An attacker can get a permissive allowlist applied to another request, letting script tags or event-handler attributes through to stored or rendered output (cross-site scripting).","severity":"medium","cvss":4.7,"epss":null,"exploited":false,"exploited_at":null,"products":[{"name":"rails-html-sanitizer","fixed":["1.7.2"],"affected":"< 1.7.2","ecosystem":"rubygems"}],"ecosystems":["rubygems"],"tags":["cwe-362","cwe-79","multi-threaded","package","puma","rails","ruby"],"not_affected_if":["custom-scrubber","single-configuration","single-threaded"],"how_to_check":"- Check the installed gem version with `bundle exec gem list rails-html-sanitizer` or in Gemfile.lock; affected below `1.7.2`.\n- Confirm the app runs more than one thread per process (for example Puma with multiple threads).\n- Search the code for `sanitize` calls that pass `tags:` or `attributes:` options, or that use no options while other calls differ.","fix":"- rails-html-sanitizer: move to `1.7.2`","status":"open","published_at":"2026-11-13T18:09:16+00:00","first_seen_at":"2026-11-13T18:12:12+00:00","triaged_at":"2026-11-13T18:13:40+00:00","updated_at":"2026-11-13T18:13:40+00:00","category":"package","confidence":0.85,"triage":{"at":"2026-11-13T18:13:40Z","model":"claude-haiku-5-5","output":{"fix":"- rails-html-sanitizer: move to `1.7.2`","reach":"internet","title":"rails-html-sanitizer: shared scrubber state lets concurrent requests bypass allowlists","impact":"medium","summary":"rails-html-sanitizer shares one scrubber across sanitize calls in a process, so concurrent calls with different allowlists can interleave. An attacker can get a permissive allowlist applied to another request, letting script tags or event-handler attributes through to stored or rendered output (cross-site scripting).","audience":"developers","category":"package","nicknames":[],"who_cares":["rails","ruby","multi-threaded","puma"],"confidence":0.85,"fix_status":"available","needs_auth":"unknown","worst_case":"Stored script injected into other users' pages via cross-site scripting","analyst_note":"","how_to_check":"- Check the installed gem version with `bundle exec gem list rails-html-sanitizer` or in Gemfile.lock; affected below `1.7.2`.\n- Confirm the app runs more than one thread per process (for example Puma with multiple threads).\n- Search the code for `sanitize` calls that pass `tags:` or `attributes:` options, or that use no options while other calls differ.","plain_action":"","product_name":"","needs_analyst":false,"not_affected_if":["single-threaded","single-configuration","custom-scrubber"],"who_is_affected":"Rails applications that run multiple threads per process and call sanitize with different tags or attributes options, including calls that use defaults. Applications using one allowlist everywhere, passing their own scrubber, or running single-threaded are not affected.","user_interaction":"no","product_ecosystem":"","needs_special_setup":"yes","severity_if_unknown":"medium"},"cost_usd":0.000815,"input_tokens":5192,"output_tokens":592,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-128069':16A,24A '-2026':15A,23A '-362':30B '-79':32B '1.7.2':148C,154C '458c':21A 'across':47B,105C 'action':156C 'admit':229C 'affect':147C,150C,264C,320C 'allowlist':58B,67B,145C,201C,224C,228C,325C 'anoth':70B,211C,287C 'appli':68B 'applic':262C,302C 'assign':113C 'attack':62B,203C 'attribut':79B,117C,131C,236C,282C,343C,349C 'built':334C 'call':13A,49B,55B,108C,120C,126C,137C,143C,171C,195C,239C,278C,288C,292C,329C,336C 'caus':214C 'code':193C 'concurr':12A,54B,124C,209C 'configur':307C 'credit':351C 'cross':87B,247C 'cross-sit':86B,246C 'cve':14A,22A 'cwe':29B,31B 'default':301C 'differ':57B,128C,285C 'emit':240C 'event':77B,234C 'event-handl':76B,233C 'everi':106C,169C,327C 'everywher':308C 'exampl':183C 'fix':152C 'get':64B 'ghsa':18A 'ghsa-h7wh-q2qr-458c':17A 'h7wh':19A 'hackeron':356C 'handler':78B,235C 'helper':163C 'hiume':354C 'html':3A,27A,42B,92C,103C,338C 'html4':96C 'html5':98C 'impact':155C 'includ':290C 'instanc':112C 'instead':345C 'interleav':60B,134C 'let':72B 'memoiz':158C 'modul':164C 'multi':34B,179C 'multi-thread':33B,178C 'none':151C 'one':45B,101C,136C,271C 'option':132C,283C,296C,350C 'output':85B,254C 'packag':36B 'pass':279C,294C,309C,331C 'path':194C,208C 'per':162C,273C,335C 'permiss':66B,200C,223C,227C 'permitscrubb':104C,190C 'permitscrubber.new':339C 'possibl':5A 'process':52B,174C,274C 'puma':37B,184C 'q2qr':20A 'rail':2A,38B,41B,91C,102C,337C 'rails':26A 'rails-html-sanit':1A,40B 'rails-html-sanitizer':25A 'render':84B,258C 'report':352C 'request':71B,186C,212C,217C 'rubi':39B 'run':268C,314C 'safelistsanit':93C,161C 'sanit':4A,43B,48B,107C,125C,170C,196C,220C,277C,328C 'sanitizer':28A 'script':73B,89B,230C,249C 'scrub':139C 'scrubber':9A,46B,312C,333C 'server':181C 'set':344C 'share':8A,44B,100C,187C 'singl':160C,306C,316C 'single-thread':315C 'site':88B,248C 'state':10A 'store':82B,256C 'strict':238C,253C 'summari':90C 'synchron':122C 'tag':74B,115C,129C,280C,341C,347C 'thread':35B,180C,272C,317C 'trigger':206C 'two':123C 'unchang':242C 'use':166C,299C,304C,322C 'user':261C 'variant':99C 'vector':250C 'version':146C,153C 'via':7A,355C 'view':157C 'without':121C 'workaround':321C 'xss':6A","nicknames":[],"mitigations":[],"who_affected":"Rails applications that run multiple threads per process and call sanitize with different tags or attributes options, including calls that use defaults. Applications using one allowlist everywhere, passing their own scrubber, or running single-threaded are not affected.","tier":"C","verified":{},"analyst_revisit_at":null,"fix_status":"available","audience":"developers","plain_action":null,"pkeys":["railshtmlsanitizer"],"ptokens":["html","rails","sanitizer"],"labels":["cwe362","cwe79","medium","multithreaded","package","puma","rails","ruby","rubygems"],"campaign_type":null,"search_a":"'-128069':16A,24A '-2026':15A,23A '458c':21A 'call':13A 'concurr':12A 'cve':14A,22A 'ghsa':18A 'ghsa-h7wh-q2qr-458c':17A 'h7wh':19A 'html':3A,27A 'possibl':5A 'q2qr':20A 'rail':2A 'rails':26A 'rails-html-sanit':1A 'rails-html-sanitizer':25A 'sanit':4A 'sanitizer':28A 'scrubber':9A 'share':8A 'state':10A 'via':7A 'xss':6A","search_b":"'-362':30B '-79':32B 'across':47B 'allowlist':58B,67B 'anoth':70B 'appli':68B 'attack':62B 'attribut':79B 'call':49B,55B 'concurr':54B 'cross':87B 'cross-sit':86B 'cwe':29B,31B 'differ':57B 'event':77B 'event-handl':76B 'get':64B 'handler':78B 'html':42B 'interleav':60B 'let':72B 'multi':34B 'multi-thread':33B 'one':45B 'output':85B 'packag':36B 'permiss':66B 'process':52B 'puma':37B 'rail':38B,41B 'rails-html-sanit':40B 'render':84B 'request':71B 'rubi':39B 'sanit':43B,48B 'script':73B,89B 'scrubber':46B 'share':44B 'site':88B 'store':82B 'tag':74B 'thread':35B","search_c":"'1.7.2':148C,154C 'across':105C 'action':156C 'admit':229C 'affect':147C,150C,264C,320C 'allowlist':145C,201C,224C,228C,325C 'anoth':211C,287C 'applic':262C,302C 'assign':113C 'attack':203C 'attribut':117C,131C,236C,282C,343C,349C 'built':334C 'call':108C,120C,126C,137C,143C,171C,195C,239C,278C,288C,292C,329C,336C 'caus':214C 'code':193C 'concurr':124C,209C 'configur':307C 'credit':351C 'cross':247C 'cross-sit':246C 'default':301C 'differ':128C,285C 'emit':240C 'event':234C 'event-handl':233C 'everi':106C,169C,327C 'everywher':308C 'exampl':183C 'fix':152C 'hackeron':356C 'handler':235C 'helper':163C 'hiume':354C 'html':92C,103C,338C 'html4':96C 'html5':98C 'impact':155C 'includ':290C 'instanc':112C 'instead':345C 'interleav':134C 'memoiz':158C 'modul':164C 'multi':179C 'multi-thread':178C 'none':151C 'one':101C,136C,271C 'option':132C,283C,296C,350C 'output':254C 'pass':279C,294C,309C,331C 'path':194C,208C 'per':162C,273C,335C 'permiss':200C,223C,227C 'permitscrubb':104C,190C 'permitscrubber.new':339C 'process':174C,274C 'puma':184C 'rail':91C,102C,337C 'render':258C 'report':352C 'request':186C,212C,217C 'run':268C,314C 'safelistsanit':93C,161C 'sanit':107C,125C,170C,196C,220C,277C,328C 'script':230C,249C 'scrub':139C 'scrubber':312C,333C 'server':181C 'set':344C 'share':100C,187C 'singl':160C,306C,316C 'single-thread':315C 'site':248C 'store':256C 'strict':238C,253C 'summari':90C 'synchron':122C 'tag':115C,129C,280C,341C,347C 'thread':180C,272C,317C 'trigger':206C 'two':123C 'unchang':242C 'use':166C,299C,304C,322C 'user':261C 'variant':99C 'vector':250C 'version':146C,153C 'via':355C 'view':157C 'without':121C 'workaround':321C","cause_entry":null,"cause_path":null,"disclosures_checked_at":null,"first_public_at":"2026-11-02T06:07:43+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:L/A:N","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-h7wh-q2qr-458c","url":"https://github.com/advisories/GHSA-h7wh-q2qr-458c","severity":"medium","published_at":"2026-11-13T18:09:16+00:00","modified_at":"2026-11-13T18:09:17+00:00","first_seen_at":"2026-11-13T18:12:12+00:00","fetched_at":"2026-11-13T18:13:46+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-128069","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-128069","severity":null,"published_at":"2026-11-13T18:17:34+00:00","modified_at":"2026-11-13T18:17:34+00:00","first_seen_at":"2026-11-13T18:49:29+00:00","fetched_at":"2026-11-13T18:49:32+00:00","note":null}],"documents":[{"id":148215,"url":"https://api.github.com/repos/rails/rails-html-sanitizer/security-advisories/GHSA-h7wh-q2qr-458c","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":148216,"url":"https://github.com/rails/rails-html-sanitizer/security/advisories/GHSA-h7wh-q2qr-458c","title":"Maintainer's advisory: github.com","kind":"vendor","source":"ghsa","publisher":"vendor","text":null,"published_at":"2026-11-02T06:07:43+00:00","relation":"advisory","confidence":null,"note":"Maintainer's advisory: github.com"},{"id":148217,"url":"https://github.com/rails/rails-html-sanitizer/commit/15f764c3563d5dd9b97ad32eaa7454577ccec4b9","title":"Patch: github.com","kind":"other","source":"ghsa","publisher":"vendor","text":null,"published_at":null,"relation":"fix","confidence":null,"note":"Patch: github.com"},{"id":148177,"url":"https://github.com/rails/rails-html-sanitizer/releases/tag/v1.7.2","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"}