CVE-2026-128069: rails-html-sanitizer: Possible XSS via shared scrubber state in concurrent calls
Summary
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.
- Versions affected: < 1.7.2 - Not affected: none - Fixed versions: 1.7.2
Impact
Action 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.
Applications 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.
Workarounds
Use 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.
Credit
Reported by hiumee via HackerOne.
Other sources
rails-html-sanitizer is responsible for sanitizing HTML fragments in Rails applications. Before 1.7.2, the HTML4 and HTML5 variants of Rails::HTML::SafeListSanitizer reuse one Rails::HTML::PermitScrubber across calls and mutate its tags and attributes without synchronization. In a multithreaded process, concurrent sanitize calls using different tags: or attributes: options can interleave and cause one call to use the other call's allowlist. If a permissive call admits a script element or event-handler attribute, output expected to be sanitized under a stricter policy can retain it and cause cross-site scripting when rendered. Applications that are single-threaded, use one identical allowlist everywhere, or provide a separate scrubber: are not affected. This issue is fixed in version 1.7.2.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
rubygems/rails-html-sanitizerto a version that resolves this vulnerability.Fixed in 1.7.2 - Upgrade
Upgrade
rails-html-sanitizerto a version that resolves this vulnerability.Fixed in 1.7.2 - Configuration
Avoid passing differing tags: or attributes: options across concurrent sanitize calls; use one identical allowlist everywhere or provide a separately built scrubber for each call.
Rails::HTML::SafeListSanitizer sanitize allowlist = Use the same allowlist for every call, or pass a per-call Rails::HTML::PermitScrubber with tags and attributes set
Event History
Frequently Asked Questions
Which deployments are exposed to this issue?
Applications using rails-html-sanitizer before 1.7.2 are affected. Exploitation additionally requires a multi-threaded deployment in which more than one request can call sanitize concurrently on the shared sanitizer instance.
What must an attacker be able to do to exploit it?
An attacker must be able to trigger a request that uses a more permissive tags or attributes allowlist concurrently with a request using a stricter allowlist. The race can cause the stricter request to use the permissive allowlist instead.
When does this become an XSS risk?
The more permissive allowlist must permit script or an event-handler attribute, and the output from the strict sanitization path must be stored or rendered to other users. In that case, content that should have been removed can be emitted unchanged.
What should be done if upgrading is not immediately possible?
Avoid calling sanitize with different per-call tags or attributes allowlists on concurrently executed requests, especially allowlists that permit script elements or event-handler attributes. Reducing execution to a single thread also removes the concurrent-call condition described for exploitation.
How can I determine whether my application has a relevant exposure path?
Review sanitize call sites for tags: or attributes: options and identify any paths with a more permissive allowlist than others. Then verify whether those paths can run concurrently with strict sanitization calls in a multi-threaded server process.