CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



GHSA-2J22-PR5W-6GQ8
6.5

GHSA-2j22-pr5w-6gq8: Cross-Site Scripting Filter Bypass in Loofah allowed_uri?

Amit Schendel
Amit Schendel
Senior Security Researcher

Mar 27, 2026·6 min read·1 visit

PoC Available

Executive Summary (TL;DR)

Loofah 2.25.0 fails to properly sanitize HTML entity-encoded control characters in URIs evaluated via allowed_uri?. This permits XSS attacks via crafted javascript: URIs. Upgrading to 2.25.1 resolves the issue.

The Loofah Ruby library version 2.25.0 contains an improper URI validation vulnerability in the allowed_uri? method. Attackers can bypass Cross-Site Scripting (XSS) filters by injecting HTML entity-encoded control characters into URI schemes. This allows execution of arbitrary JavaScript when the maliciously crafted URI is rendered and interacted with in a web browser.

Vulnerability Overview

Loofah is a widely used Ruby library for manipulating and sanitizing HTML and XML documents. It provides various helper methods to ensure that untrusted user input does not contain malicious structures. One such method is Loofah::HTML5::Scrub.allowed_uri?, which determines whether a given URI string uses an acceptable protocol.

Version 2.25.0 of the loofah gem introduced a flaw in how this helper method processes control characters. The vulnerability, tracked as GHSA-2j22-pr5w-6gq8, allows an attacker to bypass the protocol validation checks. This bypass specifically targets filters designed to block javascript: and vbscript: URI schemes.

The defect constitutes an Improper URI Validation vulnerability that directly leads to a Cross-Site Scripting (XSS) condition. Applications are vulnerable if they directly invoke the allowed_uri? method on user-supplied strings and subsequently render those strings as clickable links. The default sanitize() method remains unaffected due to differences in the underlying parsing engine.

Root Cause Analysis

The allowed_uri? method utilizes a specific sequence of operations to evaluate the safety of a URI. In version 2.25.0, the function first removes literal control characters from the input string using a regular expression. Following this removal, the method invokes CGI.unescapeHTML to decode any HTML entities present in the string.

The vulnerability stems from this specific order of operations. By stripping control characters before decoding HTML entities, the method fails to account for control characters that are entirely encoded. For example, an attacker can input 
 to represent a carriage return without triggering the initial literal character filter.

After the entity decoding phase, the now-literal control characters are embedded within the URI string. The method concludes by checking the string against a blocklist regular expression, URI_PROTOCOL_REGEX. The presence of the newly decoded control character disrupts the pattern matching, causing the regex to fail and the URI to be incorrectly classified as safe.

Modern web browsers perform URI normalization prior to execution. When a browser encounters a scheme containing embedded control characters, such as java\rscript:, it automatically strips the invalid characters. This normalization converts the bypassed string back into a functional javascript: payload, executing the embedded code.

Code Analysis

An examination of lib/loofah/html5/scrub.rb in version 2.25.0 reveals the flawed sequence. The method processes the uri_string by chaining string manipulation methods. The initial gsub(CONTROL_CHARACTERS, "") targets raw input, completely missing encoded characters.

The vulnerable code path executes CGI.unescapeHTML after the first substitution.

def allowed_uri?(uri_string)
  uri_string = CGI.unescapeHTML(uri_string.gsub(CONTROL_CHARACTERS, ""))
    .gsub(":", ":")
    .downcase
  # Protocol regex check follows
end

This implementation leaves the decoded string without a subsequent sanitization pass. Any control character instantiated during the unescapeHTML phase remains in the string through the final return statement.

The patch introduced in commit f4ebc9c5193dde759a57541062e490e86fc7c068 implements a double-pass sanitization strategy. The developers modified the method to perform a second gsub operation immediately after the HTML decoding step.

def allowed_uri?(uri_string)
  uri_string = CGI.unescapeHTML(uri_string.gsub(CONTROL_CHARACTERS, ""))
    .gsub(CONTROL_CHARACTERS, "") # Second pass removes characters decoded from entities
    .gsub(":", ":")
    .downcase
end

This remediation ensures that any control characters revealed by entity decoding are neutralized before the protocol evaluation. The initial substitution is retained to prevent malformed entities from interfering with the unescapeHTML process. The protocol regex now correctly identifies and blocks malicious schemes regardless of their initial encoding state.

Exploitation

Exploiting this vulnerability requires sending a specifically crafted payload to an application that utilizes the allowed_uri? helper. The attacker constructs a javascript: or vbscript: URI and inserts an HTML entity corresponding to a control character. Valid payloads documented in the security research include 	 (Tab), 
 (Line Feed), and 
 (Carriage Return).

Hexadecimal entity equivalents, such as 	 and 
, also successfully bypass the filter. A standard payload resembles java
script:alert(1). When the application passes this string to the vulnerable Loofah method, it returns a boolean true, indicating the URI is safe for rendering.

A simple Ruby script demonstrates the bypass in version 2.25.0.

require 'loofah'
payload = "java
script:alert('XSS')"
# Returns true in 2.25.0
puts Loofah::HTML5::Scrub.allowed_uri?(payload)

The vulnerability exhibits a specific constraint regarding its exploitability in production environments. The default Loofah.sanitize() method utilizes Nokogiri for initial HTML parsing. Nokogiri automatically decodes HTML entities prior to Loofah evaluating the URI attributes, meaning the literal control characters are present and successfully stripped by the first phase of the vulnerable function. Exploitation is strictly limited to applications making direct calls to the allowed_uri? helper.

Impact Assessment

The successful exploitation of this vulnerability results in Cross-Site Scripting (XSS). An attacker can execute arbitrary JavaScript within the context of the victim's session. This code execution allows for the theft of authentication cookies, session hijacking, and unauthorized actions performed on behalf of the user.

The scope of the impact is constrained by the application's implementation details. The vulnerability requires the application to manually pass user-controlled input to the allowed_uri? method. Applications relying solely on the standard sanitize() document parsing flow are not exposed to this specific bypass technique.

The GitHub Security Advisory provides a CVSS v4.0 base score of 6.5. This score reflects the network-based attack vector and the lack of required privileges. The assessment accounts for the requirement of user interaction, as the victim must interact with the rendered URI to trigger the payload execution.

Confidentiality and integrity impacts are rated as low in the official CVSS vector, consistent with standard XSS vulnerabilities. The availability of the application remains unaffected. Despite the specific prerequisites for exploitation, the availability of a public Proof-of-Concept warrants prompt remediation.

Remediation

The primary mitigation strategy is upgrading the loofah dependency to version 2.25.1. The maintainers released this patched version to correct the order of operations in the URI validation logic. Organizations should execute bundle update loofah to ensure the patched gem is integrated into their applications.

Security teams must verify the deployed version across all environments. Static analysis tools and software composition analysis (SCA) scanners should be configured to flag version 2.25.0. The Gemfile.lock provides a definitive record of the installed version for auditing purposes.

For codebases where immediate patching is not feasible, developers must implement a manual workaround. Applications should avoid passing unparsed, user-supplied strings directly to the allowed_uri? helper. Developers can manually sanitize the input by applying a rigorous control character stripping routine prior to validation.

A comprehensive code review should be conducted to identify direct invocations of Loofah::HTML5::Scrub.allowed_uri?. Reviewers must analyze the data flow to determine if untrusted input reaches the function without prior normalization. Migrating custom validation logic to the default Loofah.sanitize() method provides a more robust defense against similar bypass techniques.

Official Patches

FlavorjonesPatch commit implementing double-pass filter
FlavorjonesRelease v2.25.1 containing the fix

Fix Analysis (1)

Technical Appendix

CVSS Score
6.5/ 10
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N/E:U

Affected Systems

RubyGems ecosystemApplications directly calling Loofah::HTML5::Scrub.allowed_uri?

Affected Versions Detail

Product
Affected Versions
Fixed Version
loofah
Flavorjones
== 2.25.02.25.1
AttributeDetail
CWE IDCWE-79
Attack VectorNetwork
CVSS v4.06.5
Exploit StatusProof-of-Concept
Vulnerable ComponentLoofah::HTML5::Scrub.allowed_uri?
Vulnerable Version2.25.0
Fixed Version2.25.1
Patch Commitf4ebc9c5193dde759a57541062e490e86fc7c068

MITRE ATT&CK Mapping

T1189Drive-by Compromise
Initial Access
T1204.001User Execution: Malicious Link
Execution
T1190Exploit Public-Facing Application
Initial Access
CWE-79
Cross-site Scripting

Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

Known Exploits & Detection

Official Test SuiteProof of Concept payloads included in the repository test suite.

Vulnerability Timeline

Fix commit f4ebc9c5 merged by maintainer
2026-03-17
Initial advisory GHSA-46fp-8f5p-pf2m published
2026-03-18
Secondary advisory GHSA-2j22-pr5w-6gq8 published and updated
2026-03-26

References & Sources

  • [1]GitHub Security Advisory GHSA-2j22-pr5w-6gq8
  • [2]Alias Advisory GHSA-46fp-8f5p-pf2m
  • [3]Fix Commit f4ebc9c5193dde759a57541062e490e86fc7c068
  • [4]Loofah v2.25.1 Release
  • [5]Ruby Advisory Database Entry
  • [6]OSV Entry for GHSA-2j22-pr5w-6gq8

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.