CVE
CVE-2026-126203, CVE-2026-126205, CVE-2026-126206, CVE-2026-126207, CVE-2026-126208, CVE-2026-126209, CVE-2026-128068, CVE-2026-128069, CVE-2026-126348, CVE-2026-126349, CVE-2026-126350, CVE-2026-126351, CVE-2026-126352, CVE-2026-126354, CVE-2026-126366, CVE-2026-128084
CWE
CWE-79, CWE-436, CWE-22, CWE-1333, CWE-407, CWE-918, CWE-362, CWE-451, CWE-770, CWE-190
Affected Surface
- RubyGems `loofah` before 2.25.3 (`CVE-2026-126203` from 2.21.0, `CVE-2026-126209` from 2.25.0, `CVE-2026-126206` only in 2.25.2), and any app that uses it through `rails-html-sanitizer`, Action View or Action Text
- RubyGems `rails-html-sanitizer` before 1.7.2 (`CVE-2026-128069` all versions, `CVE-2026-128068` from 1.0.3); the 1.6.x and 1.5.x branches no longer get security fixes
- RubyGems `addressable` before 2.9.1 (`CVE-2026-126354` from 2.2.5, template ReDoS from 2.3.0, IDNA display spoofing from 2.0.0)
- RubyGems `msgpack` java platform gem through 1.8.6 on JRuby (`CVE-2026-126366`, fixed in 1.8.7)
- RubyGems `sqlite3` through 2.10.1 on 64-bit platforms (`CVE-2026-128084`, fixed in 2.10.2)
TL;DR
On 12 and 13 November 2026, GitHub published CVEs for 16 advisories in Ruby gems that sit under most Rails apps. The maintainers had shipped the fixes between 26 October and 2 November, so patched versions are already on RubyGems. Every advisory is rated medium or lower apart from one JRuby-only High, and none is reported as exploited. Most of the advisories include a working payload, though, and the gems are hard to avoid: Loofah has about 775 million downloads, rails-html-sanitizer about 735 million and Addressable about 1.3 billion.
The ones worth reading closely:
- rails-html-sanitizer shares one allowlist across threads (
CVE-2026-128069). Action View memoizes a singleSafeListSanitizerper helper module, and that sanitizer reused onePermitScrubber, overwriting itstagsandattributeson every call. Under Puma, a request that callssanitize(html, tags: %w[...])with a looser allowlist can race a stricter call and hand it the looser rules. - Loofah HTML5 mutation XSS (
CVE-2026-126203). Nokogiri before 1.19.5 left<and>unescaped inside attribute values. With certain MathML in the output, a browser re-parses the sanitized string and turns the attribute contents into live markup. The:stripscrubber is affected in every browser;:pruneand:escapein Safari and other WebKit browsers. - Loofah
allowed_uri?bypasses (CVE-2026-126209,CVE-2026-126206).javascript: alert(1)anddata:text/html,...passed the string-level URL check. Loofah’s normalsanitizepath is not affected, only code that callsallowed_uri?(orRails::HTML::Sanitizer.allowed_uri?) directly. - Addressable resolves
..wrongly (CVE-2026-126354)./../../public/x/../../secretnormalized to/public/x/secretinstead of/secret. An app that checks the normalized path against an allowlist and then forwards the raw URI can be walked past the allowlist. The same release fixes quadratic path normalization that affects every Ruby version, three URI template ReDoS bugs and two IDN display spoofs. - msgpack-ruby on JRuby (
CVE-2026-126366, CVSS 7.5). A 5-bytearray32header made the Java decoder allocatenew IRubyObject[0x7FFFFFFF]up front.
What to do now: run bundle update loofah rails-html-sanitizer nokogiri addressable and confirm you land on Loofah 2.25.3+, Nokogiri 1.19.5+, rails-html-sanitizer 1.7.2+ and Addressable 2.9.1+. JRuby users should move to msgpack 1.8.7; anyone who builds SQL from large user input should move to sqlite3 2.10.2. Loofah 2.25.3 and rails-html-sanitizer 1.7.2 require Ruby 3.2, so apps still on Ruby 3.1 or older can’t take the fix without upgrading Ruby first.
Affected packages
| Ecosystem | Package | CVE (issue) | Severity | Affected versions | Fixed version |
|---|---|---|---|---|---|
| RubyGems | loofah | CVE-2026-126203 (HTML5 mutation XSS) | Medium, CVSS 3.1 6.1 | >= 2.21.0, < 2.25.3 (or Nokogiri < 1.19.5) | 2.25.3 with Nokogiri 1.19.5 |
| RubyGems | loofah | CVE-2026-126209 (allowed_uri? semicolon-less reference) | Medium, CVSS 4.0 5.3 | >= 2.25.0, < 2.25.3 | 2.25.3 |
| RubyGems | loofah | CVE-2026-126206 (allowed_uri? / in data: mediatype) | Medium, CVSS 4.0 5.3 | 2.25.2 only | 2.25.3 |
| RubyGems | loofah | CVE-2026-126207 (CSS url\28...\29 escape) | Medium, CVSS 3.1 4.7 | < 2.25.3 | 2.25.3 |
| RubyGems | loofah | CVE-2026-126208 (SVG textPath/mpath external href) | Medium, CVSS 3.1 4.7 | < 2.25.3 | 2.25.3 |
| RubyGems | loofah | CVE-2026-126205 (SVG href ReDoS, Ruby 3.1 and older) | Medium, CVSS 3.1 5.3 | < 2.25.3 | 2.25.3 |
| RubyGems | rails-html-sanitizer | CVE-2026-128069 (shared scrubber race) | Medium, CVSS 3.1 4.7 | < 1.7.2 | 1.7.2 |
| RubyGems | rails-html-sanitizer | CVE-2026-128068 (SVG href ReDoS) | Medium, CVSS 3.1 5.3 | >= 1.0.3, < 1.7.2 | 1.7.2 |
| RubyGems | addressable | CVE-2026-126354 (dot-segment allowlist bypass) | Medium, CVSS 3.1 4.8 | >= 2.2.5, < 2.9.1 | 2.9.1 |
| RubyGems | addressable | CVE-2026-126351 (quadratic normalize_path) | Medium, CVSS 3.1 5.9 | < 2.9.1 | 2.9.1 |
| RubyGems | addressable | CVE-2026-126352, CVE-2026-126350 (template ReDoS) | Medium, CVSS 3.1 5.9 | >= 2.3.0, < 2.9.1 | 2.9.1 |
| RubyGems | addressable | CVE-2026-126348, CVE-2026-126349 (IDN display_uri spoofing) | Medium 4.3, Low 3.1 | >= 2.0.0, < 2.9.1 | 2.9.1 |
| RubyGems | msgpack (java platform) | CVE-2026-126366 (JRuby decoder OOM) | High, CVSS 3.1 7.5 | <= 1.8.6 | 1.8.7 |
| RubyGems | sqlite3 | CVE-2026-128084 (SQL truncation at 4 GiB) | Medium, CVSS 3.1 4.8 | <= 2.10.1 | 2.10.2 |
Technical analysis
1. rails-html-sanitizer: one scrubber for every thread (CVE-2026-128069)
Rails::HTML::SafeListSanitizer built its PermitScrubber once, in the constructor, and assigned the per-call allowlist onto that shared object. Action View keeps one sanitizer instance per helper module for the life of the process, so every sanitize call in a multi-threaded server goes through the same scrubber. The fix in commit 15f764c3 builds a scrubber per call:
def initialize(prune: false)
- @permit_scrubber = PermitScrubber.new(prune: prune)
+ @prune = prune
end
def scrub(fragment, options = {})
...
elsif allowed_tags(options) || allowed_attributes(options)
- @permit_scrubber.tags = allowed_tags(options)
- @permit_scrubber.attributes = allowed_attributes(options)
- fragment.scrub!(@permit_scrubber)
+ permit_scrubber = PermitScrubber.new(prune: @prune)
+ permit_scrubber.tags = allowed_tags(options)
+ permit_scrubber.attributes = allowed_attributes(options)
+ fragment.scrub!(permit_scrubber)
In the old code, thread A sets tags to a permissive list, thread B sets its strict list, thread A’s assignment lands again, and B’s scrub! runs with A’s list. If any code path in the app allows script or an on* attribute (an admin-only rich-text preview, say), an attacker who can hit that path at the same time as a public comment render can get the script through the public sanitizer. Apps that use the same allowlist everywhere, pass their own scrubber:, or run one thread per process are not affected.
2. Loofah: three ways past the HTML5 scrubbers
Mutation XSS (CVE-2026-126203). The HTML standard says serializers must escape < and > inside attribute values. Nokogiri didn’t before 1.19.5 (sparklemotion/nokogiri#3741). Loofah’s output was safe as a DOM, but once a browser parsed the serialized string again, MathML context could make it read markup out of an attribute value. The advisory says the :strip scrubber is affected in every browser and :prune/:escape in WebKit. Loofah’s HTML4 API, :whitewash, rails-html-sanitizer’s default configuration and JRuby are not affected. Loofah 2.25.3 simply requires Nokogiri 1.19.5 or newer, which is why it also drops Ruby older than 3.2.
Semicolon-less character references (CVE-2026-126209). allowed_uri? stripped whitespace and control characters before decoding numeric references. A browser ends : at the space that follows it and reads a colon, while Loofah removed the space first, so the reference ran into the next letter and decoded as Ϊ (U+03AA). No scheme was found and the URI was approved. The fix in commit 62c04841 decodes first, as a browser does:
- uri_string = decode_numeric_character_references(CGI.unescapeHTML(uri_string.gsub(CONTROL_CHARACTERS, "")))
+ uri_string = CGI.unescapeHTML(decode_numeric_character_references(uri_string))
uri_string.gsub!(CONTROL_CHARACTERS, "")
CVE-2026-126206 is a near neighbour, introduced in 2.25.2: CGI.unescapeHTML doesn’t decode HTML5 named references such as /, so data:text/html,<script>... looked like a malformed mediatype and fell back to the allowed text/plain. Both bugs only matter to code that validates a user URL with Loofah::HTML5::Scrub.allowed_uri? or Rails::HTML::Sanitizer.allowed_uri? and then writes it into an href. The normal sanitize path gets entities decoded by Nokogiri before the check runs.
Request leaks without script (CVE-2026-126207, CVE-2026-126208). scrub_css checked for url() on the first parse of a style attribute, then rebuilt each property with CSS escapes decoded. list-style: url\28 https://attacker.example/\29 passed the check and came out as a live url(). Separately, the SVG local-reference list spelled textPath in lowercase (the HTML5 parser names it textPath inside <svg>) and left out mpath, so external hrefs on those elements survived; Firefox fetches them. Either way the viewer’s browser makes a request to an attacker-chosen URL, which leaks IP address and timing and can reach hosts only routable from the viewer’s network.
3. Addressable: .. above the root kept the wrong segments (CVE-2026-126354)
Addressable::URI.normalize_path used a regex rewrite loop over RFC 3986 section 5.2.4. Once a .. climbed above the root, every later /../../ was removed as a pair, so segments that should have been dropped were kept. URI#normalize, #join, #== and #=== all call it. Commit 25c7c412 replaces the loop with one pass over the segments:
- normalized_path = path.dup
- loop do
- mod ||= normalized_path.gsub!(RULE_2A, SLASH)
- pair = normalized_path.match(RULE_2B_2C)
- ...
+ segments = path.split(SLASH, -1)
+ return path.dup if segments.empty?
+ # The first segment is never removed by a parent reference. For an
+ # absolute path it is the empty segment before the leading slash.
+ output = [segments.first]
+ last = segments.size - 1
+ (1..last).each do |index|
+ segment = segments[index]
+ if segment == SELF_REF
+ output << EMPTY_STR if index == last
+ elsif segment == PARENT
+ if output.size > 1
+ output.pop
The vulnerable pattern looks like this:
uri = Addressable::URI.parse(params[:target]) # "/../../public/x/../../secret"
if uri.normalize.path.start_with?("/public/") # 2.9.0 says "/public/x/secret"
proxy_to(upstream, params[:target]) # upstream resolves it to "/secret"
end
The commit message also notes a second bug: the old rules used ^ and $, which match around a raw line feed in Ruby, so joining "..\n/secret" onto /public/ gave /\n/secret. The same single-pass rewrite fixes CVE-2026-126351. Removing one dot segment per pass made normalization quadratic, and a 34 KB path took about 3.5 seconds on MRI 4.0. Unlike the template ReDoS bugs, this one affects every Ruby version.
The two template ReDoS bugs (CVE-2026-126352, CVE-2026-126350) only matter if you call Template#match or #extract on user input. CVE-2026-126350 is an incomplete fix: the 2.9.0 patch for CVE-2026-35611 never tested inputs that start with the operator’s leading character, so {/segments*} was still exponential on MRI older than 3.2 and on JRuby. The IDN bugs only affect display_uri/to_unicode output. For example, https://xn--google-.example.org/ displays as google.example.org with the pure-Ruby IDNA implementation.
4. msgpack-ruby on JRuby: allocation sized by the attacker (CVE-2026-126366)
The C extension has capped container pre-allocation since 1.7.3 and capped it by buffered bytes since 1.8.6. The Java decoder used for the java platform gem had neither cap. "\xdd" + [0x7FFFFFFF].pack("N") made consumeArray run new IRubyObject[0x7FFFFFFF] before reading any element, and str32/bin32 did the same through readBytes. The reporter measured a silent band too: a 2×10⁷ count with no elements grew heap by 78 MiB with no exception. Commit ca0efc12 mirrors the C fix:
private IRubyObject consumeArray(int size) {
- IRubyObject[] elements = new IRubyObject[size];
+ int preallocatedSize = size;
+ if (preallocatedSize > buffer.remaining()) {
+ preallocatedSize = buffer.remaining();
+ }
+ RubyArray array = runtime.newArray(preallocatedSize);
...
private byte[] readBytes(int size) {
- byte[] payload = new byte[size];
- buffer.get(payload);
- return payload;
+ if (buffer.remaining() >= size) {
+ byte[] payload = new byte[size];
+ buffer.get(payload);
+ return payload;
+ }
+ throw new BufferUnderflowException();
CRuby users of msgpack are not affected by this advisory. The reporter also states that Fluentd, which runs on CRuby only, is not affected.
5. sqlite3-ruby: lengths cast to int (CVE-2026-128084)
sqlite3-ruby passed Ruby string lengths to SQLite as int. On 64-bit platforms a SQL string of 4 GiB or more wrapped, so SQLite saw only length mod 2^32 bytes and could drop a trailing WHERE clause from an inlined query. 2.10.2 raises RangeError for anything longer than INT_MAX. The maintainers rate it 4.8 because an attacker has to get 4 GiB into one string first.
Exposure indicators
These are library bugs, not malware, so there are no network IOCs. Payload shapes to search for in stored content and request logs:
javascript:orگollowed by a space or tab inside a URL field (CVE-2026-126209)data:URIs containing/,:,	or
(CVE-2026-126206)styleattributes containingurl\28or other CSS-escaped parentheses (CVE-2026-126207)<textPathor<mpathwith anhrefthat does not start with#(CVE-2026-126208)- Request paths with a
..segment that climbs above the root followed by more..segments, such as/../../public/x/../../(CVE-2026-126354) - msgpack frames starting
\xdd,\xdbor\xc6with a declared length far larger than the frame (CVE-2026-126366)
Detection and remediation
Find the resolved versions in every lockfile:
grep -nE '^ (loofah|rails-html-sanitizer|nokogiri|addressable|msgpack|sqlite3) \(' Gemfile.lock
bundle exec ruby -e 'require "loofah"; require "nokogiri"; puts Loofah::VERSION, Nokogiri::VERSION'
bundle exec gem list -e addressable
Upgrade, then confirm:
bundle update loofah rails-html-sanitizer nokogiri addressable msgpack sqlite3 --conservative
bundle exec bundle-audit check --update # the Ruby Advisory Database already carries these IDs
If bundle update refuses because of Ruby version constraints, you are on Ruby 3.1 or older. Loofah 2.25.3 and rails-html-sanitizer 1.7.2 need Ruby 3.2+. Until you can upgrade Ruby:
- For
CVE-2026-128069, make everysanitizecall use the same allowlist, or passscrubber: Rails::HTML::PermitScrubber.new.tap { |s| s.tags = ...; s.attributes = ... }per call. Find candidates withgrep -rnE 'sanitize\([^)]*(tags|attributes):' app/. - For
CVE-2026-126203, upgrade Nokogiri alone to 1.19.5 if your Ruby supports it, or switch HTML5 call sites to the HTML4 API or:whitewash. - For the
allowed_uri?bypasses, grep for direct callers:grep -rn 'allowed_uri?' app/ lib/. - For
CVE-2026-126354, reject any path that contains..(including%2e%2e) before an access decision, or use Addressable’s normalized value for both the check and the outgoing request. - For the template ReDoS bugs, wrap
Template#matchand#extracton user input in a short timeout.
No credentials need rotating for these bugs. If you ran an affected rails-html-sanitizer under a threaded server and allow script or event-handler attributes anywhere, review stored user content rendered through the strict path for script tags.
Timeline
- 2026-09-13: rails-html-sanitizer commits the per-call
PermitScrubberfix (HackerOne report 3585784). - 2026-10-26: sqlite3-ruby 2.10.2 released; GHSA-4fqr-96q8-xrfx published by the maintainers.
- 2026-10-27: msgpack 1.8.6 caps CRuby pre-allocation; the JRuby decoder is still uncapped.
- 2026-10-30: Addressable 2.9.1 and msgpack 1.8.7 released; Addressable and msgpack-ruby publish their advisories.
- 2026-11-02: Loofah 2.25.3 (05:02 UTC) and rails-html-sanitizer 1.7.2 (06:07 UTC) released with their advisories; Nokogiri 1.19.5 is the required floor.
- 2026-11-12 (
19:16 UTC):CVE-2026-126203published to NVD. - 2026-11-12 (
20:52to22:55 UTC): GitHub adds the Loofah, Addressable and msgpack advisories to its reviewed database. - 2026-11-13 (
17:54to18:57 UTC): GitHub adds the rails-html-sanitizer and sqlite3-ruby advisories;CVE-2026-128068andCVE-2026-128069reach NVD.
References
- Loofah GHSA-h6v6-rg66-fqj4 (CVE-2026-126203)
- Loofah v2.25.3 release notes
- Loofah commit 62c04841 (CVE-2026-126209)
- Nokogiri PR #3741
- rails-html-sanitizer GHSA-h7wh-q2qr-458c (CVE-2026-128069)
- rails-html-sanitizer commit 15f764c3
- Addressable GHSA-8gmh-2hhj-2hv9 (CVE-2026-126354)
- Addressable commit 25c7c412
- msgpack-ruby GHSA-73v3-wf97-vpgj (CVE-2026-126366)
- sqlite3-ruby GHSA-4fqr-96q8-xrfx (CVE-2026-128084)
- Ruby Advisory Database: loofah
- NVD CVE-2026-126203
From research to remediation
Check whether this pattern exists in your codebase
Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.
References
- Loofah GHSA-h6v6-rg66-fqj4: mutation XSS in the HTML5 scrubbers through unescaped attribute values (CVE-2026-126203)
- Loofah GHSA-p9wj-w8xq-vjx6 and commit 62c04841: allowed_uri? accepts semicolon-less numeric references (CVE-2026-126209)
- Loofah v2.25.3 release notes
- Nokogiri v1.19.5: escape < and > in HTML attribute values (sparklemotion/nokogiri#3741)
- rails-html-sanitizer GHSA-h7wh-q2qr-458c: XSS via shared scrubber state in concurrent calls (CVE-2026-128069)
- rails-html-sanitizer commit 15f764c3: build a PermitScrubber per SafeListSanitizer call
- rails-html-sanitizer GHSA-cpgf-5mjf-73p7: SVG href ReDoS with overridden allowlists (CVE-2026-128068)
- Addressable GHSA-8gmh-2hhj-2hv9: normalize_path resolves dotted segments to the wrong path (CVE-2026-126354)
- Addressable commit 25c7c412: normalize paths in a single pass (CVE-2026-126354, CVE-2026-126351)
- Addressable GHSA-j82w-28jv-3pmj: explode-modifier template ReDoS left open by the 2.9.0 fix (CVE-2026-126350)
- msgpack-ruby GHSA-73v3-wf97-vpgj and commit ca0efc12: JRuby decoder allocates from the declared header count (CVE-2026-126366)
- sqlite3-ruby GHSA-4fqr-96q8-xrfx: SQL truncation for strings of 4 GiB or more (CVE-2026-128084)
- Ruby Advisory Database entries for loofah, rails-html-sanitizer, addressable and sqlite3
- NVD CVE-2026-126203