Nov 12, 2026·9 min read·3 visits
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.
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`.
Loofah is a widely deployed Ruby library built on top of Nokogiri for manipulating, transforming, and sanitizing HTML and XML documents and fragments. Within the Ruby on Rails ecosystem, Loofah serves as the underlying sanitization engine for rails-html-sanitizer. In addition to full DOM-tree scrubbing, Loofah provides standalone utility methods for validating individual strings, notably Loofah::HTML5::Scrub.allowed_uri?, which is re-exported directly as Rails::HTML::Sanitizer.allowed_uri?.
In Loofah version 2.25.2, a parser differential and input neutralization flaw (classified under CWE-79 and CWE-184) exists in Loofah::HTML5::Scrub.allowed_uri?. Applications invoke this method to verify whether a user-supplied URI string uses a permitted scheme and, in the case of data: URIs, a permitted MIME mediatype before embedding the string into HTML attributes such as href or src.
Because allowed_uri? relies on Ruby's standard library CGI.unescapeHTML for entity decoding, it only resolves legacy HTML4/XML named entities and numeric character references. It fails to decode the HTML5 named character reference /, which represents the solidus character / (U+002F). When an attacker supplies a data: URI whose mediatype separator is encoded as / (for example, data:text/html,<script>alert(1)</script>), Loofah treats the mediatype as malformed and falls back to the safe text/plain default. When the validated string is rendered into an HTML attribute, the browser's HTML5 parser decodes / to / prior to URI processing, resulting in arbitrary JavaScript execution in the context of the victim's browser session.
> [!NOTE]
> This vulnerability only affects applications that call Loofah::HTML5::Scrub.allowed_uri? or Rails::HTML::Sanitizer.allowed_uri? directly on unparsed strings. Standard DOM sanitization workflows using Loofah.scrub_fragment or Loofah.fragment(html).scrub! are not affected because Nokogiri's HTML parser decodes all character references before invoking URI scrubbers.
To understand the root cause of CVE-2026-126206, it is necessary to examine both the WHATWG data: URL parsing specification and how HTML5 parsers handle named character references inside attribute values. According to the WHATWG Data URL specification, a data: URI follows the syntax data:[<mediatype>][;base64],<data>, where <mediatype> is structured as type/subtype*(;parameter). If the <mediatype> string does not contain a literal solidus (/) separating the type and subtype, the MIME parser rejects the mediatype as malformed and defaults to text/plain;charset=US-ASCII.
In Loofah::HTML5::Scrub.allowed_uri?, data: URIs are inspected to ensure their mediatype matches an explicit safelist (such as image/png, image/gif, or text/plain). Mediatypes capable of active script execution, specifically text/html and image/svg+xml, are rejected. Before parsing the scheme and mediatype, allowed_uri? normalizes the input string to prevent attackers from smuggling prohibited schemes or mediatypes using HTML character references that a browser would later decode when parsing an HTML attribute.
In version 2.25.2, the normalization pipeline executed the following sequence: strip control characters, decode numeric character references and legacy named entities via CGI.unescapeHTML, strip any newly decoded control characters, strip HTML5 whitespace character references (	 and 
), and replace : with :. However, CGI.unescapeHTML only recognizes the classic HTML4 entity table (&, <, >, ", ') and numeric references. It leaves modern HTML5 named character references intact.
Across the entire HTML5 named character reference specification, exactly four named references resolve to characters that act as structural delimiters or stripped whitespace during URI parsing:
: (U+003A :): Delimits the URI scheme./ (U+002F /): Separates the MIME type and subtype in data: URIs and delimits path segments.	 (U+0009): Stripped by WHATWG URL parsers.
 (U+000A): Stripped by WHATWG URL parsers.While Loofah 2.25.2 explicitly accounted for :, 	, and 
, it omitted /. When data:text/html,<script>alert(1)</script> was evaluated, the mediatype substring text/html lacked a / character, causing Loofah's mediatype check to evaluate it as text/plain and approve the URI.
The vulnerability resides in lib/loofah/html5/scrub.rb within the allowed_uri? class method. In Loofah 2.25.2, the colon replacement was hardcoded as a single string substitution (uri_string.gsub!(":", ":")), leaving / unhandled during string normalization.
Maintainer Mike Dalessio (@flavorjones) resolved the issue in commit ad92002966053309f83b550861e2ef04bc714744 (Pull Request #324) by introducing a constant mapping for all URI delimiter character references (DELIMITER_CHARACTER_REFERENCES) and applying a unified regular expression substitution.
# File: lib/loofah/html5/scrub.rb
# 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)
def allowed_uri?(uri_string)
# Control characters are stripped both before and after decoding, since decoding can
# produce them. That strip must 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, "")
# PATCHED: Replaces both : and / with their literal delimiter characters
uri_string.gsub!(DELIMITER_CHARACTER_REFERENCE, DELIMITER_CHARACTER_REFERENCES)
uri_string.downcase!
# ... scheme and data: mediatype validation continues ...
endThe ordering of operations in allowed_uri? is critical to the completeness of this fix. First, raw control characters are removed, and CGI.unescapeHTML alongside decode_numeric_character_references resolves any numeric or double-encoded entities (such as &sol; or �). Second, control characters and named whitespace references (	, 
) are stripped before DELIMITER_CHARACTER_REFERENCE is evaluated.
If delimiter substitution occurred prior to control character or whitespace stripping, an attacker could split the entity name (for example, data:text&s�ol;html or data:text&s	ol;html) so that it would coalesce into / only after the delimiter replacement pass had already completed. Furthermore, uri_string.downcase! is intentionally called after delimiter replacement; HTML5 named character references are case-sensitive, meaning browsers do not decode uppercase &SOL; as /, and preserving case until after entity replacement matches browser tokenization behavior.
Exploiting CVE-2026-126206 requires an application to accept a user-controlled URL string, validate it via Loofah::HTML5::Scrub.allowed_uri? or Rails::HTML::Sanitizer.allowed_uri?, and subsequently render that string into an HTML attribute without re-encoding the ampersand (&) character. This pattern occurs in custom view helpers, markdown link renderers, or rich-text link validators where developers validate URLs prior to constructing raw HTML tags.
Consider a vulnerable Ruby on Rails helper method that validates user-supplied profile URLs:
def render_user_link(user_url)
if Rails::HTML::Sanitizer.allowed_uri?(user_url)
"<a href=\"#{user_url}\">Website</a>".html_safe
else
"<span>Invalid URL</span>".html_safe
end
endAn attacker can supply any of several payload variants to bypass the allowed_uri? check in Loofah 2.25.2:
data:text/html,<script>alert(document.domain)</script>data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==data:text/html,<script>alert(1)</script>data:text&s�ol;html,<script>alert(1)</script>
data:text&s	ol;html,<script>alert(1)</script>
data:text&s
ol;html,<script>alert(1)</script>When the application renders <a href="data:text/html,<script>alert(document.domain)</script>">Website</a> (or embeds the URI into an <iframe> or <object> src/data attribute), the browser's HTML parser processes the attribute value and resolves / to /. If rendered in an <iframe> or <embed>, the script executes automatically upon page load; if rendered in an <a href> tag, the script executes when a user clicks the link (depending on browser navigation context).
The security impact of CVE-2026-126206 is Cross-Site Scripting (XSS) within the client browser session. The vulnerability is scored at 5.3 (Medium) under CVSS v4.0 (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). Because exploitation occurs within the user's browser rather than on the server, the Vulnerable System Impact metrics (VC/VI/VA) are rated as None (N), while the Subsequent System Impact metrics for Confidentiality and Integrity (SC/SI) are rated as Low (L).
Successful exploitation allows an unauthenticated remote attacker (AV:N/PR:N) with low attack complexity (AC:L/AT:N) and passive user interaction (UI:P) to execute arbitrary JavaScript in the context of the victim's session. Depending on where the data: URI is placed in the DOM, an attacker can steal session identifiers not protected by HttpOnly flags, read sensitive DOM content, perform unauthorized state-changing requests via Cross-Site Request Forgery (CSRF) bypass, or present fraudulent authentication prompts.
The scope of affected environments is constrained by two factors. First, the flaw was introduced in Loofah 2.25.2 (released July 15, 2026) and remediated in 2.25.3, meaning earlier versions of the library are not affected by this specific regression. Second, standard Rails view helpers (such as link_to) HTML-escape attribute values by default (turning / into &sol;), and standard Loofah DOM scrubbers decode entities via Nokogiri prior to calling allowed_uri?. Consequently, only codebases that invoke allowed_uri? directly and insert the unescaped string into an HTML attribute are vulnerable.
Engineering and security teams should first determine whether Loofah 2.25.2 is present in their dependency tree and whether allowed_uri? is invoked directly within application or library code.
To audit the installed version and search for direct callers, execute the following commands in the project root:
# Check Gemfile.lock for Loofah 2.25.2
bundle list | grep loofah
bundle exec bundler-audit check --update
# Search application code for direct calls to allowed_uri?
grep -rn "Loofah::HTML5::Scrub.allowed_uri?" app/ lib/ config/
grep -rn "Rails::HTML::Sanitizer.allowed_uri?" app/ lib/ config/Upgrade the loofah gem to version 2.25.3 or later, which includes commit ad92002966053309f83b550861e2ef04bc714744:
# Gemfile
gem "loofah", ">= 2.25.3"Then update the locked dependency:
bundle update loofahIf an immediate gem upgrade is blocked by deployment freezes, apply an initializer patch that runs after Loofah::HTML5::Scrub is loaded to ensure / is decoded during URI validation:
# config/initializers/loofah_cve_2026_126206_patch.rb
require "loofah"
selected_constants = Loofah::HTML5::Scrub
if Loofah::VERSION == "2.25.2"
Loofah::HTML5::Scrub.singleton_class.prepend(
Module.new do
def allowed_uri?(uri_string)
return super if uri_string.nil?
# Pre-normalize control chars, whitespace refs, and / before delegating
patched = uri_string.gsub(Loofah::HTML5::Scrub::CONTROL_CHARACTERS, "")
patched = Loofah::HTML5::Scrub.send(:decode_numeric_character_references, CGI.unescapeHTML(patched))
patched.gsub!(Loofah::HTML5::Scrub::CONTROL_CHARACTERS, "")
patched.gsub!(Loofah::HTML5::Scrub::WHITESPACE_CHARACTER_REFERENCES, "")
patched.gsub!("/", "/")
super(patched)
end
end
)
endAdditionally, developers should avoid concatenating validated URI strings directly into .html_safe interpolation blocks; instead, use standard Rails tag helpers (tag.a, link_to) or full DOM fragment sanitization via Loofah.scrub_fragment.
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| Product | Affected Versions | Fixed Version |
|---|---|---|
loofah flavorjones | = 2.25.2 | 2.25.3 |
| Attribute | Detail |
|---|---|
| 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 |
The software does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVE-2026-126188 (GHSA-3f4q-8x5m-76v8) is an information disclosure vulnerability in the Active Storage component of Ruby on Rails prior to versions 8.0.5.2 and 8.1.4.1. When generating previews or extracting metadata from uploaded media files, Active Storage invokes the external command-line tools ffmpeg and ffprobe without specifying a format whitelist. An attacker who can upload a crafted HTTP Live Streaming (HLS) playlist while declaring a standard video or audio MIME type can cause FFmpeg to engage the HLS demuxer, resolve file:// segment URIs, and disclose visual frames or metadata from local media files readable by the Rails application process.
pgEdge RAG Server versions prior to 2.0.0 contain a race condition in the hybrid search pipeline orchestrator (CWE-362, CWE-488). The server maintains a single shared, in-memory BM25 index instance across all incoming HTTP requests on a pipeline. Concurrent queries to the unauthenticated HTTP REST API interleave non-atomic clear, document addition, and search operations on this singleton index. As a result, documents fetched using one caller's search filter can be queried and returned to another concurrent caller, bypassing multi-tenant data isolation without generating any anomalous audit trail in server or database logs.
CVE-2026-118740 is a Missing Authorization (CWE-862) vulnerability in the Microsoft Aspire Dashboard and Aspire.Dashboard.Sdk.linux-x64 packages. In versions 13.6.1 prior to 13.6.5, an architectural flaw in the Blazor routing configuration (Routes.razor) causes unmapped (404 / Not-Found) routes to bypass the AuthorizeRouteView component. By navigating to an arbitrary non-existent route, an unauthenticated remote attacker forces the application into the NotFound template, which unconditionally renders the primary MainLayout component and initializes global user interface services, interaction providers, and underlying SignalR/Blazor circuits without verifying authentication credentials.
An elevation of privilege vulnerability exists in .NET Aspire versions prior to 13.6.5 due to the insertion of sensitive information into sent data (CWE-201). When the Developer Control Plane (DCP) container tunneling feature is enabled, the orchestration engine implicitly exposes all endpoints of the Aspire Dashboard—including its administrative frontend and management APIs—to containerized workloads instead of restricting tunnel access solely to OpenTelemetry Protocol (OTLP) collection endpoints. This exposure allows an attacker operating within a compromised or untrusted container to query the dashboard frontend, extract sensitive environment variables and connection strings, and elevate privileges across the host orchestration boundary.
CVE-2026-118750 is an uncontrolled resource allocation vulnerability (CWE-770) affecting the Microsoft.AspNetCore.WebUtilities component in ASP.NET Core. Specifically, the FileBufferingReadStream class eagerly allocates large memory buffers from the shared ArrayPool<byte> inside its constructor prior to reading any incoming request payload data. An unauthenticated remote attacker can exploit this behavior by opening numerous concurrent connections and stalling or omitting request body transmission, exhausting shared memory pools and causing a server-wide Denial of Service (DoS).
CVE-2026-114155 is a local elevation of privilege vulnerability affecting Microsoft .NET on Linux and macOS platforms. The vulnerability exists within the dotnet-watch (Hot Reload) component and is caused by a Time-of-Check Time-of-Use (TOCTOU) race condition (CWE-367) during the generation and execution of temporary XML solution files (.slnx) in shared system temporary directories. A local attacker with low privileges can intercept and modify the temporary solution file before MSBuild evaluates it, achieving arbitrary code execution in the security context of the user running dotnet-watch.