Skip to content
Security & Trust

Rails sanitizer can apply one request's loose rules to another's

A shared-state flaw in rails-html-sanitizer can let a strict filter run with a permissive allowlist in multi-threaded apps

W
WebPulse Newsroom
AI-assisted · 4 min read
Share on X LinkedIn
Rails sanitizer can apply one request's loose rules to another's
In brief
  • A GitHub advisory says rails-html-sanitizer shares one scrubber across calls, so concurrent requests can be filtered with each other's allowlist, which may allow cross-site scripting.
  • Only multi-threaded apps whose sanitize calls use differing tags: or attributes: options are affected, including calls that rely on defaults.
  • Teams should audit their sanitize calls, make the allowlists match or build a scrubber per call, and review the linked v1.7.2 release.

A filter is only as strict as the state it shares

Many security failures are obvious ones: someone leaves a control off. This one fails more quietly. The control is on and set up correctly. In a multi-threaded app, it can borrow another request's settings.

That is the lesson in a GitHub Advisory Database entry published on November 13, 2026. It covers rails-html-sanitizer, a Ruby library. The advisory is GHSA-h7wh-q2qr-458c. It describes possible cross-site scripting, where a page ends up carrying an attacker's script. The text provided gives no CVE ID.

Think of two people writing their rules on one shared whiteboard at the same moment. One of them then follows the other's rules. Neither did anything wrong alone. The design allowed the mix-up. In the real flaw, an attacker also has to trigger the looser path.

How the flaw works

The library cleans user-supplied HTML. It strips any tag or attribute that is not on an allowlist, meaning a list of what is permitted.

The advisory says the HTML4 and HTML5 versions of Rails::HTML::SafeListSanitizer share one PermitScrubber. That is the component that does the stripping. Every call on the same instance uses it.

On each call, the sanitizer sets the scrubber's tags and attributes. It does this without any synchronization. Two calls running together with different options can interleave. One call then gets cleaned with the other call's list.

The advisory adds that Action View, the Rails view layer, keeps one sanitizer per helper module. Every sanitize call in the process uses it. On a multi-threaded server such as Puma, all requests share the same scrubber.

What an attacker needs

The advisory describes the path. Imagine one part of an app cleans input with a looser list. An attacker who can trigger that part, while another request is being cleaned, can make the other request use the looser list.

Say that looser list allows script or an event-handler attribute. The strict call then lets it through. The advisory calls this a cross-site scripting vector when the output is stored or shown to other users.

The outcome depends on timing. The advisory does not say how easy it is to trigger in practice. It reports no exploitation, and this story claims none.

Who is and is not exposed

The advisory narrows the scope. An app is affected only with more than one thread per process. It also needs a sanitize call whose tags: or attributes: options differ from another call's.

One detail matters for audits. A call with no options counts, because it uses the defaults. A team might clean comments strictly and one admin field loosely. It could meet both conditions without realising.

The advisory names three groups that are not affected. These are apps with one configuration everywhere, apps that pass their own scrubber: object, and single-threaded apps.

What leaders should ask their teams

The advisory gives two fixes. The first is to make every sanitize call use an identical allowlist. The second is to build a scrubber for each call. That means Rails::HTML::PermitScrubber.new with its tags and attributes set directly, in place of the tags: and attributes: options.

The advisory also links the v1.7.2 release notes for rails-html-sanitizer. The text provided does not say what that release changes. Teams should read the notes before relying on it.

Four questions will give a quick answer.

First, do our Rails apps run Puma, or any server with more than one thread per process? Second, where do we call sanitize? Do those calls use different tags: or attributes: options, or none at all? Third, does any path allow a looser list for trusted users, such as an editor or admin tool? Fourth, is cleaned output stored and later shown to other people?

If the first two answers are yes, the last two set the priority. The cheapest fix is often to make the allowlists identical.

The wider point

Teams check what a security control is set to do. They check less often what it shares with its neighbours. A filter that is strict on paper can be loose at the moment it matters. That happens when its rules sit where other requests can reach them.

So the practical audit is simple: list every sanitize call, and check whether any two use different allowlists.

Produced by the WebPulse Newsroom with AI assistance from the original reporting credited below, and checked against that source by our editorial review. How we use AI.
Original reporting: GitHub Advisory Database.