fix: TOCTOU race in resolveWorkspacePath allows symlink-based path traversal - #531
fix: TOCTOU race in resolveWorkspacePath allows symlink-based path traversal#531Sunil56224972 wants to merge 1 commit into
Conversation
…idation When filepath.EvalSymlinks returns os.IsNotExist, the old code returned the unresolved fullPath directly. Between this validation and the subsequent os.ReadFile/os.Open, an attacker could create a symlink at that path pointing outside the repo. The symlink would be followed without any WithinBase re-check, leaking arbitrary file content. Fix: propagate the not-found error instead of returning the unvalidated path. All callers handle file-not-found errors gracefully already.
|
🔍 OpenCodeReview found 1 issue(s) in this PR.
|
| // the subsequent os.ReadFile/os.Open, a symlink could be placed | ||
| // at fullPath pointing outside the repo, and it would be | ||
| // followed without any WithinBase verification. | ||
| return "", fmt.Errorf("resolve file %q: %w", path, err) |
There was a problem hiding this comment.
This is a security-critical change to resolveWorkspacePath, but the test file (filereader_test.go) has no tests covering this function's behavior. Consider adding tests that verify:
- Non-existent files now return an error (the changed behavior)
- Symlinks pointing outside the repo are rejected
- Valid files within the repo still resolve correctly
- Path traversal attempts (e.g.,
../../etc/passwd) are rejected
Without tests, future refactors could accidentally reintroduce the TOCTOU vulnerability.
|
Thanks for the contribution! The security concern is thoughtful, but I have a few reservations about this change: Behavior regression for the common case Before this patch, when a user (or the LLM) requests a file that simply does not exist, the error path is:
After this patch, the error becomes:
The original message is semantically clear ("read failed because the file does not exist"), while the new one leaks an implementation detail about path resolution. For the LLM consumer of these errors, the former is more understandable. On the TOCTOU concern If the file truly does not exist (not even as a symlink), Suggestion If you would still like to harden this path, a better approach might be using I would lean toward not merging this as-is due to the UX regression for a theoretical concern. Happy to discuss further! |
|
Thanks for the thoughtful review! You raise valid points: On the UX regression — You're right. The original error message ( On the threat model — Fair point that in a typical code review workflow, files come from a git checkout and the TOCTOU window is extremely narrow. The scenario requires an attacker with real-time filesystem access during the review, which is not a realistic attack surface for most deployments. Your Lstat suggestion is better — Distinguishing 'nothing exists' (safe, let caller report clean file-not-found) from 'broken symlink exists' (reject) preserves the original UX while still hardening the edge case. I agree this is the right approach if the team wants to harden it later. Given your analysis, I'm okay with this PR being closed. The original behavior is indeed fine for the actual threat model. Thanks for explaining the reasoning — learned something about balancing security hardening vs UX in LLM-facing tooling! 🙏 |
Summary
Security Fix: TOCTOU (Time-of-Check-to-Time-of-Use) race condition in
resolveWorkspacePath()(internal/tool/filereader.go) allows symlink-based path traversal to read arbitrary files outside the repository.Root Cause
When
filepath.EvalSymlinks(fullPath)returnsos.IsNotExist, the function returns the unresolvedfullPathdirectly:The caller (
readFromDisk/readLinesFromDisk) then callsos.ReadFile(fullPath)oros.Open(fullPath). Between theEvalSymlinkscheck and the actual file read, an attacker can:fullPathpointing to a sensitive file outside the repo (e.g.,/etc/shadow,~/.ssh/id_rsa)WithinBase), but its target is outsideos.ReadFilefollows the symlink and reads the target — noWithinBasere-check happensImpact
An attacker with the ability to create symlinks within the repository (e.g., via a malicious commit in a PR being reviewed) could cause OCR to read and leak arbitrary file contents from the host system into the LLM conversation. This is particularly dangerous in CI/CD environments where the runner may have access to secrets, SSH keys, or cloud credentials.
Fix
Remove the
os.IsNotExistspecial-case that returns the unvalidated path. Instead, propagate the error to the caller. All callers (readFromDisk,readLinesFromDisk) already handle file-not-found errors gracefully by returning an error message to the LLM, so no behavioral change for legitimate use cases.Attack Scenario