fix(upload): allowlist filesystem paths and validate tabId#14
fix(upload): allowlist filesystem paths and validate tabId#14sergei-aronsen wants to merge 1 commit into
Conversation
Security Review: PR #14 — fix(upload): allowlist filesystem paths and validate tabIdReviewed commit: 1. Path Allowlist Implementation (
|
| Attack Vector | Mitigated? | Notes |
|---|---|---|
| Symlink from inside root to outside | YES | realpathSync resolves before prefix check |
../ traversal in filePath |
YES | realpathSync canonicalizes |
| Null bytes in path | YES | Node.js fs APIs throw on null bytes since v18+ |
| Very long paths | PARTIAL | OS-level PATH_MAX applies, no explicit check |
| Unicode normalization attacks | YES | realpathSync works on the filesystem's canonical form |
| Environment variable manipulation | N/A | If attacker can set COMET_UPLOAD_ROOT=/, they win — but that requires process-level access |
| Race condition (TOCTOU) | PARTIAL | See finding above |
3.2 Can an attacker bypass tabId validation?
| Attack Vector | Mitigated? | Notes |
|---|---|---|
CDP HTTP API traversal (/json/version) |
YES | Slashes not in charset |
| Injection of extra CDP params | YES | Only hex + hyphens allowed |
| Excessively long IDs | YES | Max 64 chars |
| Empty string | YES | Min 16 chars |
4. Code Quality
- Dead code removal (
connectionCheckInterval) — clean and correct. - Error handling — both validation functions throw with clear messages, caught at call sites.
- The
resolvedPathvariable is correctly used for the actual upload call instead of the originalfilePath. This is the critical detail — using the validated canonical path prevents post-validation manipulation.
5. Verdict
APPROVE — This PR materially improves security posture against LLM-driven file exfiltration (the primary threat model). The implementation is sound:
- Symlink resolution is correctly applied before path comparison
- Path separator handling prevents prefix-confusion bypasses
- tabId validation blocks CDP HTTP API surface traversal
- Error paths are handled gracefully
The findings above are informational/low severity and do not block merge. The TOCTOU race is theoretical given the threat model (untrusted LLM prompts, not local attacker with fs access).
Recommendations for follow-up (non-blocking):
- Consider adding
~/.netrc,~/.npmrc,~/.bash_historyto the denylist - Consider a file size limit (
COMET_UPLOAD_MAX_SIZE) - Tighten tabId regex to require at least one hex digit between hyphens
Security-focused review at commit 48a193c.
|
This PR has merge conflicts with main after recent merges (PRs #11, #12, #16, #18 landed). Conflicting files: Resolution (trivial — keep both sides):
Please rebase against main. |
`comet_upload` accepted any local path and forwarded it to
`DOM.setFileInputFiles` against whatever page Comet was on. Combined
with indirect prompt-injection from page content, this is an
arbitrary-file exfiltration vector: a malicious page can instruct the
agent to call `comet_upload {filePath: "/Users/x/.ssh/id_rsa"}` and
the file lands in the next form submission.
The fix is a layered allowlist:
1. **Opt-in `COMET_UPLOAD_ROOT` env**: when set, resolve symlinks with
`realpathSync` and require the canonical path to live under the
configured root. Anything outside is rejected with a user-facing
message.
2. **Hardcoded denylist** (active when the env is unset, to keep the
default permissive behaviour for existing users): refuse uploads
from `~/.ssh`, `~/.aws`, `~/.config/{gh,gcloud}`, `~/.gnupg`,
`~/.kube`, `~/.docker`, `~/Library/Keychains` (macOS), `/etc/shadow`,
`/etc/sudoers`, `/root`. These have no plausible upload use case.
3. **Warning log** when the env is unset, so users notice the default
is permissive and can flip on the allowlist.
Symlink resolution defeats `~/uploads/sneak -> ~/.ssh/id_rsa` style
bypasses. `statSync` confirms it's a regular file (not a FIFO or
character device).
Also tightened `comet_tabs action=switch|close tabId=...`: validate
the value matches `/^[A-Fa-f0-9-]{16,64}$/` before passing to
`Target.connect` / `Target.closeTarget`. CDP target ids are 32-char
hex; bogus values otherwise produce cryptic errors and can be coaxed
into traversing the local CDP HTTP API surface (`/json/version`, …).
Removed the dead `connectionCheckInterval` field on `CometCDPClient`
— declared but never assigned/cleared.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
48a193c to
56e6146
Compare
Summary
`comet_upload` accepted any local path and forwarded it to `DOM.setFileInputFiles` against whatever page Comet was on. Combined with indirect prompt-injection from page content, this is an arbitrary-file exfiltration vector: a malicious page can instruct the agent to call `comet_upload {filePath: "/Users/x/.ssh/id_rsa"}` and the file lands in the next form submission.
Fix
Layered allowlist:
/.ssh`, `/.aws`, `/.config/{gh,gcloud}`, `/.gnupg`, `/.kube`, `/.docker`, `~/Library/Keychains`, `/etc/shadow`, `/etc/sudoers`, `/root`.Symlink resolution defeats `~/uploads/sneak -> ~/.ssh/id_rsa` style bypasses. `statSync` confirms it's a regular file.
Also tightened `comet_tabs action=switch|close tabId=...`: validate value matches `/^[A-Fa-f0-9-]{16,64}$/` before passing to `Target.connect` / `Target.closeTarget`. CDP target ids are 32-char hex; bogus values otherwise produce cryptic errors and can be coaxed into traversing the local CDP HTTP API surface (`/json/version`, …).
Removed dead `connectionCheckInterval` field on `CometCDPClient` — declared but never assigned/cleared.
Compatibility
Default behaviour unchanged for any caller not uploading from a sensitive system path. The new env var is opt-in.
Test plan
🤖 Generated with Claude Code