fix: sanitize plugin manifest name in plugin-manager add to prevent p…#671
fix: sanitize plugin manifest name in plugin-manager add to prevent p…#671RanWurmbrand wants to merge 1 commit into
Conversation
…ath traversal and plugin override Signed-off-by: Ran Wurmbrand <rwurmbra@redhat.com>
📝 WalkthroughWalkthroughPlugin binary installation now validates manifest filenames for path traversal and existing targets before performing retrieval or filesystem writes. ChangesPlugin installation validation
Estimated code review effort: 2 (Simple) | ~10 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Test Coverage ReportTotal: 49.1% Per-package coverage
Full function-level detailsPosted by CI |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cmd/plugin-manager/add/add.go`:
- Around line 217-221: Update the file creation logic in downloadBinary to
include os.O_EXCL alongside the existing creation flags, so opening the target
fails atomically if a file or symlink appears during download; also use
executable mode 0755 instead of 0777.
- Around line 205-215: The validateFileInput function must use os.Lstat to
inspect the destination without following symlinks, and treat any error other
than os.ErrNotExist as a validation failure. Preserve the existing
duplicate-file error for an existing path, while rejecting broken symlinks and
propagating unexpected filesystem errors before installation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 407cee60-227b-49d0-9410-e2eae0785a98
📒 Files selected for processing (1)
cmd/plugin-manager/add/add.go
| func validateFileInput(filepath, filename string) error { | ||
| if strings.Contains(filename, "/") || strings.Contains(filename, "\\") || strings.Contains(filename, "..") { | ||
| return fmt.Errorf("invalid plugin name %q: must not contain path separators or traversal sequences", filename) | ||
| } | ||
|
|
||
| existingPath := filepath + "/" + filename | ||
| if _, err := os.Stat(existingPath); err == nil { | ||
| return fmt.Errorf("a plugin named %q already exists at %s, please remove it first before installing", filename, existingPath) | ||
| } | ||
| return nil | ||
| } |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Check current imports in add.go to determine if path/filepath and errors need to be added.
rg '^import ' -A 15 cmd/plugin-manager/add/add.goRepository: migtools/crane
Length of output: 411
🏁 Script executed:
sed -n '180,280p' cmd/plugin-manager/add/add.go
python3 - <<'PY'
import os, tempfile, pathlib, stat
with tempfile.TemporaryDirectory() as d:
p = pathlib.Path(d) / "broken"
try:
p.symlink_to(pathlib.Path(d) / "missing")
except (OSError, NotImplementedError) as e:
print("symlink_unavailable", type(e).__name__, e)
else:
try:
os.stat(p)
except OSError as e:
print("stat", type(e).__name__, e.errno)
try:
os.lstat(p)
except OSError as e:
print("lstat", type(e).__name__, e.errno)
PYRepository: migtools/crane
Length of output: 3309
🏁 Script executed:
python3 - <<'PY'
import os, tempfile, pathlib
with tempfile.TemporaryDirectory() as d:
base = pathlib.Path(d)
target = base / "outside.txt"
link = base / "plugin"
link.symlink_to(target) # broken symlink
print("before_exists", target.exists())
try:
with open(link, "w") as f:
f.write("hello")
print("open_succeeded", True)
except OSError as e:
print("open_succeeded", False, type(e).__name__, e.errno)
print("after_exists", target.exists())
if target.exists():
print("target_contents", target.read_text())
PYRepository: migtools/crane
Length of output: 232
Use os.Lstat and fail closed on unexpected errors in cmd/plugin-manager/add/add.go:205-215. os.Stat follows symlinks, so a broken symlink at the destination can slip past this guard and the later os.OpenFile(..., O_CREAT|O_TRUNC, 0777) will write through it. Treat any non-os.ErrNotExist error as a failure here.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cmd/plugin-manager/add/add.go` around lines 205 - 215, The validateFileInput
function must use os.Lstat to inspect the destination without following
symlinks, and treat any error other than os.ErrNotExist as a validation failure.
Preserve the existing duplicate-file error for an existing path, while rejecting
broken symlinks and propagating unexpected filesystem errors before
installation.
Source: Coding guidelines
| func downloadBinary(filepath string, filename string, url string, log *logrus.Logger) error { | ||
| if err := validateFileInput(filepath, filename); err != nil { | ||
| return err | ||
| } | ||
|
|
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Use os.O_EXCL during file creation to prevent TOCTOU race conditions.
While validateFileInput provides a fast early check, a Time-of-Check to Time-of-Use (TOCTOU) race condition exists because the binary is downloaded over the network before the local file is actually created. If a file or symlink is created at the target path concurrently during the download, opening the file with syscall.O_TRUNC will overwrite it or follow the symlink to overwrite an arbitrary file.
To strictly enforce the protection against overwrites, use os.O_EXCL in os.OpenFile when creating the plugin binary (around line 249) to guarantee the operation fails atomically if the file already exists.
🔒 Proposed fix for file creation
Update the file creation logic (around line 249) to use os.O_EXCL and apply a safer default mode for executables (0755 instead of world-writable 0777):
- pluginBinary, err := os.OpenFile(filepath+"/"+filename, syscall.O_RDWR|syscall.O_CREAT|syscall.O_TRUNC, 0777)
+ pluginBinary, err := os.OpenFile(filepath+"/"+filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0755) // Update to use filepath.Join if parameter was renamed🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cmd/plugin-manager/add/add.go` around lines 217 - 221, Update the file
creation logic in downloadBinary to include os.O_EXCL alongside the existing
creation flags, so opening the target fails atomically if a file or symlink
appears during download; also use executable mode 0755 instead of 0777.
closes: #670
Summary
The
plugin-manager addcommand uses thenamefield from the plugin manifest as the binary filename without any sanitization. This allows a malicious plugin repository to:nameto an already-installed plugin (e.g.OpenShiftPlugin), silently replacing it with a trojanized versionnamecontaining../../sequences escapes the plugins directory, writing an executable (0777 permissions) to arbitrary pathsRoot Cause
In
downloadBinary(),value.Namefrom the manifest YAML is passed directly toos.OpenFileas the filename with no validation.Fix
Added a
validateFileInput()function that runs before any download:/,\, or..— blocks path traversalPoC
Full end-to-end proof of concept: https://github.com/RanWurmbrand/compromised-plugins
Test plan
crane plugin-manager add EvilPluginwith a manifest name containing../../is rejected with an errorcrane plugin-manager add CoolPluginwith a manifest name matching an existing plugin (OpenShiftPlugin) is rejected with an errorSummary by CodeRabbit