CVE-2026-126206: Cross-Site Scripting via HTML5 Named Solidus Entity in Loofah URI Validator
Vulnerability ID: CVE-2026-126206
CVSS Score: 5.3
Published: 2026-11-12
CVE-2026-126206 (GHSA-jcff-cj73-4w4f) is a Cross-Site Scripting (XSS) vulnerability in version 2.25.2 of the Loofah Ruby gem. The vulnerability stems from an incomplete character reference normalization routine in the Loofah::HTML5::Scrub.allowed_uri? method, which fails to decode the HTML5 named character reference / (representing the solidus character /). Consequently, a crafted data: URI such as data:text/html,<script>alert(1)</script> bypasses mediatype validation by triggering the WHATWG MIME parser fallback to text/plain, while web browsers later resolve / to / and execute the payload as text/html.
TL;DR
Loofah 2.25.2 fails to decode the HTML5 named character reference / (/) inside Loofah::HTML5::Scrub.allowed_uri? (also exposed via Rails::HTML::Sanitizer.allowed_uri?). Attackers can supply data:text/html,<script>... to bypass URI validation as text/plain and achieve Cross-Site Scripting (XSS) when the URI is rendered into an HTML attribute.
⚠️ Exploit Status: POC
Technical Details
- CVE / Advisory ID: CVE-2026-126206 / GHSA-jcff-cj73-4w4f
- Vulnerability Class: CWE-79 (Cross-Site Scripting) / CWE-184 (Incomplete List of Disallowed Inputs)
- CVSS v4.0 Score: 5.3 Medium (CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N)
- Affected Component: Loofah::HTML5::Scrub.allowed_uri? (loofah gem = 2.25.2)
- Patched Version: loofah >= 2.25.3
- Attack Vector: Network (Crafted data: URI using / HTML5 entity)
- Exploit Status: Proof-of-Concept (PoC) Publicly Available
- CISA KEV Status: Not Listed
Affected Systems
- loofah Ruby gem (version 2.25.2)
- rails-html-sanitizer (when invoking Rails::HTML::Sanitizer.allowed_uri? with loofah 2.25.2)
- loofah: = 2.25.2 (Fixed in:
2.25.3)
Code Analysis
Commit: ad92002
Ensure Loofah::HTML5::Scrub.allowed_uri? recognizes the named character reference / and rejects data: URIs that disguise a disallowed mediatype with it.
diff --git a/lib/loofah/html5/scrub.rb b/lib/loofah/html5/scrub.rb index 2a35090..e8b524a 100644 --- a/lib/loofah/html5/scrub.rb +++ b/lib/loofah/html5/scrub.rb @@ -53,6 +53,15 @@ module Scrub # URIs. CGI.unescapeHTML does not decode these, so they are handled explicitly. WHITESPACE_CHARACTER_REFERENCES = /&(Tab|NewLine);/ + # HTML5 named character references for the delimiters that decide how a URI parses: + # ":" ends the scheme, and "/" separates the type and subtype of a data: mediatype. + # CGI.unescapeHTML does not decode these either. Together with + # WHITESPACE_CHARACTER_REFERENCES these are every named character reference that + # decodes to U+0009, U+000A, U+002F, or U+003A, so no other named reference can + # change how a URI parses. + DELIMITER_CHARACTER_REFERENCES = { ":" => ":", "/" => "/" }.freeze + DELIMITER_CHARACTER_REFERENCE = Regexp.union(DELIMITER_CHARACTER_REFERENCES.keys) + class << self def allowed_element?(element_name) ::Loofah::HTML5::SafeList::ALLOWED_ELEMENTS_WITH_LIBXML2.include?(element_name) @@ -234,12 +243,12 @@ def allowed_uri?(uri_string) # also decode the semicolon-less ones, which browsers still decode and execute. Normalizing # more aggressively than a browser only rejects more, which is safe. Control characters are # stripped both before and after decoding, since decoding can produce them. That strip must - # precede WHITESPACE_CHARACTER_REFERENCES: removing a control character can reveal a named - # whitespace reference. + # precede the named reference substitutions: removing a control character can reveal a + # named reference. uri_string = decode_numeric_character_references(CGI.unescapeHTML(uri_string.gsub(CONTROL_CHARACTERS, ""))) uri_string.gsub!(CONTROL_CHARACTERS, "") uri_string.gsub!(WHITESPACE_CHARACTER_REFERENCES, "") - uri_string.gsub!(":", ":") + uri_string.gsub!(DELIMITER_CHARACTER_REFERENCE, DELIMITER_CHARACTER_REFERENCES) uri_string.downcase! if URI_PROTOCOL_REGEX.match?(uri_string) protocol = uri_string.split(SafeList::PROTOCOL_SEPARATOR)[0] Exploit Details
- GitHub Security Advisory: Official advisory containing functional proof-of-concept
data:text/htmlpayloads. - GitHub Fix Commit & Unit Tests: Upstream unit tests demonstrating plaintext, base64, double-encoded, and control-character-injected
/bypass vectors.
Mitigation Strategies
- Upgrade the
loofahRuby gem to version 2.25.3 or newer across all environments. - Prefer full DOM fragment sanitization (
Loofah.scrub_fragmentor Railssanitizehelper) over standalone string validation viaallowed_uri?. - Ensure all user-supplied URI strings rendered into HTML attributes pass through standard attribute escaping helpers (such as Rails
link_toortag.a) rather than raw.html_safestring interpolation.
Remediation Steps:
- Audit project
Gemfile.lockfiles usingbundle list | grep loofahorbundle exec bundler-audit check --updateto identify instances ofloofahversion2.25.2. - Search application source code (
app/,lib/,config/) for direct invocations ofLoofah::HTML5::Scrub.allowed_uri?orRails::HTML::Sanitizer.allowed_uri?. - Update
Gemfileto requiregem "loofah", ">= 2.25.3"and executebundle update loofah. - Run the application test suite and redeploy affected services.
References
- CVE-2026-126206 Official CVE Record
- GitHub Security Advisory GHSA-jcff-cj73-4w4f
- Loofah Fix Commit ad92002966053309f83b550861e2ef04bc714744
- Loofah Pull Request #324
- Loofah v2.25.3 Release Notes
- RubySec Advisory Database Entry for GHSA-jcff-cj73-4w4f
- Loofah GitHub Repository
Read the full report for CVE-2026-126206 on our website for more details including interactive diagrams and full exploit analysis.
