Standardize path validation across file operations to prevent path traversal#14883
Standardize path validation across file operations to prevent path traversal#14883
Conversation
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR standardizes path validation for CLI file operations by extracting an absolute-path validation helper into pkg/fileutil and refactoring several CLI paths to use it, strengthening defense-in-depth against relative path traversal.
Changes:
- Added
fileutil.ValidateAbsolutePath()helper and unit tests. - Refactored multiple CLI code paths to validate/clean absolute paths before file operations.
- Documented the required validation pattern in
CONTRIBUTING.md.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/fileutil/fileutil.go | Introduces ValidateAbsolutePath helper used to clean and enforce absolute paths. |
| pkg/fileutil/fileutil_test.go | Adds unit tests covering valid/invalid paths and common traversal patterns. |
| pkg/cli/completions.go | Replaces inline absolute-path checks with the shared helper before reading workflow files. |
| pkg/cli/commands.go | Validates workflow directory and destination file paths before creating/writing. |
| pkg/cli/trial_repository.go | Validates temp/workflow source and destination paths before stat/read/write/mkdir. |
| pkg/cli/git.go | Standardizes validation/cleaning when converting and using paths in git helpers. |
| pkg/cli/run_push.go | Validates resolved workflow paths and normalizes comparisons in push staging logic. |
| CONTRIBUTING.md | Adds “File Path Security” guidance pointing contributors to the helper. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
pkg/fileutil/fileutil.go
Outdated
| //go:build !integration | ||
|
|
There was a problem hiding this comment.
pkg/fileutil/fileutil.go is guarded by //go:build !integration, but this helper is imported by non-test CLI code. Building/running with -tags integration will exclude the only source file in the fileutil package and break compilation. Remove the build tag from the non-test source file (it’s fine to keep build tags on test files if needed).
| //go:build !integration |
|
|
||
| // Verify the path is absolute to prevent relative path traversal | ||
| if !filepath.IsAbs(cleanPath) { | ||
| return "", fmt.Errorf("path must be absolute, got: %s", path) |
There was a problem hiding this comment.
The error text includes the unquoted raw path value. Using %q (or otherwise sanitizing) makes logs/error strings unambiguous and avoids confusing output if the path contains spaces/newlines.
| return "", fmt.Errorf("path must be absolute, got: %s", path) | |
| return "", fmt.Errorf("path must be absolute, got: %q", path) |
| assert.NotEmpty(t, result, "Result should not be empty") | ||
| assert.True(t, filepath.IsAbs(result), "Result should be an absolute path: %s", result) | ||
| // Verify path is cleaned (no .. components) | ||
| assert.NotContains(t, result, "..", "Cleaned path should not contain .. components") | ||
| } |
There was a problem hiding this comment.
assert.NotContains(t, result, "..") is too broad: a valid absolute path can legally contain .. as part of a directory/file name (e.g. /var/data/..snapshot/file). If the intent is to ensure filepath.Clean removed parent-directory elements, assert that no path element equals ".." rather than doing a substring match.
| if runtime.GOOS != "windows" { | ||
| result, err := ValidateAbsolutePath(tt.path) | ||
| require.NoError(t, err, "Should not error for valid absolute path") | ||
| assert.Equal(t, tt.expected, result, "Path should be cleaned correctly") | ||
| } |
There was a problem hiding this comment.
In TestValidateAbsolutePath_Cleaning, the Windows branch silently does nothing, so the subtests still run but don’t assert anything. Consider calling t.Skip(...) when GOOS == "windows" (or splitting into OS-specific tests) so it’s clear these cases are intentionally not executed on Windows.
| if runtime.GOOS != "windows" { | |
| result, err := ValidateAbsolutePath(tt.path) | |
| require.NoError(t, err, "Should not error for valid absolute path") | |
| assert.Equal(t, tt.expected, result, "Path should be cleaned correctly") | |
| } | |
| if runtime.GOOS == "windows" { | |
| t.Skip("TestValidateAbsolutePath_Cleaning only runs on Unix-like systems") | |
| } | |
| result, err := ValidateAbsolutePath(tt.path) | |
| require.NoError(t, err, "Should not error for valid absolute path") | |
| assert.Equal(t, tt.expected, result, "Path should be cleaned correctly") |
Problem
Path validation was inconsistent across the codebase -
completions.godemonstrated proper absolute path validation with security logging, but this pattern wasn't applied to ~30 other file operations usingfilepath.Join.Changes
New validation helper (
pkg/fileutil)ValidateAbsolutePath(path string) (string, error)- performsfilepath.Cleanand absolute path checkApplied to CLI file operations (5 files, 13 locations)
completions.go- Refactored existing validation to use helpercommands.go- Workflow directory and destination file creationtrial_repository.go- Temp directories, workflow source/dest paths (5 locations)git.go- Git root lookups and path conversions (3 locations)run_push.go- Workflow and staged file path handling (3 locations)Documentation (
CONTRIBUTING.md)Usage
Blocks relative path traversal patterns:
../../../etc/passwd,.,.., etc.Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.