#23308: fix(browser): accept upload paths that traverse symlinked tmp dirs
size: S
Cluster:
Browser Security Enhancements
## Summary
- **Problem:** Browser upload rejects valid files inside \`/tmp/openclaw/uploads\` with "Invalid path: must stay within uploads directory" error on macOS.
- **Why:** On macOS, \`/tmp\` is a symlink to \`/private/tmp\`. The CLI \`normalizeUploadPaths\` resolves file paths to their real paths (e.g. \`/private/tmp/openclaw/uploads/file.png\`) via \`openFileWithinRoot\` → \`fs.realpath\`, then sends them to the gateway. The gateway's \`DEFAULT_UPLOAD_DIR\` uses the un-resolved form (\`/tmp/openclaw/uploads\`). \`path.relative\` between these produces \`../../private/tmp/...\` which the lexical containment check rejects.
- **What changed:** Added a \`realpathSync\` fallback in \`resolvePathWithinRoot\`: when the lexical check fails, resolve both root and target through \`fs.realpathSync\` to handle symlink differences. Added \`isStrictlyInsideDir\` helper to deduplicate the containment check. Added a test case for symlinked upload roots.
- **What did NOT change:** Security checks unchanged — paths must still be strictly inside the root. \`openFileWithinRoot\` (which handles symlink escapes, directory paths, etc.) is untouched. Traversal attacks still rejected.
## Change Type (select all)
- [x] Bug fix
- [ ] Feature
- [ ] Refactor
- [ ] Docs
- [ ] Security hardening
- [ ] Chore/infra
## Scope (select all touched areas)
- [ ] Gateway / orchestration
- [ ] Skills / tool execution
- [ ] Auth / tokens
- [ ] Memory / storage
- [ ] Integrations
- [ ] API / contracts
- [x] UI / DX
- [ ] CI/CD / infra
## Linked Issue/PR
- Closes #23107
## User-visible / Behavior Changes
- \`openclaw browser upload /tmp/openclaw/uploads/file.png\` now works on macOS (and other systems with symlinked tmp dirs)
## Security Impact (required)
- New permissions/capabilities? \`No\`
- Secrets/tokens handling changed? \`No\`
- New/changed network calls? \`No\`
- Command/tool execution surface changed? \`No\`
- Data access scope changed? \`No\` — The realpath fallback only accepts paths that are genuinely inside the root after symlink resolution. Traversal attacks are still caught.
## Repro + Verification
### Steps
1. On macOS: \`mkdir -p /tmp/openclaw/uploads && cp ~/file.png /tmp/openclaw/uploads/\`
2. Run \`openclaw browser upload /tmp/openclaw/uploads/file.png --input-ref <ref>\`
### Expected
- Upload succeeds
### Actual (before fix)
- "Invalid path: must stay within uploads directory (/tmp/openclaw/uploads)"
## Evidence
- All 11 existing + new tests pass (\`vitest run src/browser/paths.test.ts\`)
- New test: "accepts absolute path when root is behind a symlink" verifies the fix
## Human Verification (required)
- Verified scenarios: Symlinked root with realpath'd file, existing tests for traversal/blank/directory/symlink-escape
- Edge cases checked: File doesn't exist (parent realpath fallback), root doesn't exist (graceful fallthrough)
- What I did **not** verify: Live macOS gateway + CLI end-to-end upload
## Compatibility / Migration
- Backward compatible? \`Yes\`
- Config/env changes? \`No\`
- Migration needed? \`No\`
## Failure Recovery (if this breaks)
The realpath fallback is wrapped in try/catch. If \`realpathSync\` fails for any reason, the code falls through to the original error — identical to pre-fix behavior.
## Risks and Mitigations
Low risk. The realpath fallback is strictly additive (only runs when the lexical check already failed), preserves all security invariants, and is covered by tests.
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
Fixed macOS upload rejection caused by symlink mismatch between CLI-resolved paths (`/private/tmp/...`) and gateway's unresolved upload directory (`/tmp/...`). Added `realpathSync` fallback in `resolvePathWithinRoot` that only runs when lexical containment check fails, resolving both root and target to handle symlink differences. Security guarantees preserved—paths must still be strictly inside the root after resolution.
- Added `isStrictlyInsideDir` helper to deduplicate containment logic
- New test verifies symlinked root + realpath'd file scenario
- Fallback gracefully handles non-existent files by resolving parent directory
- All existing security tests (traversal, symlink escapes, directory paths) still pass
<h3>Confidence Score: 5/5</h3>
- Safe to merge with minimal risk
- The fix is narrowly scoped to a specific macOS symlink issue, only adds fallback logic when lexical checks already fail, preserves all security invariants (paths must still be strictly inside root), has comprehensive test coverage including security edge cases, and is wrapped in try/catch for graceful degradation
- No files require special attention
<sub>Last reviewed commit: 5e78023</sub>
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#22910: fix(browser): resolve symlinks in upload path validation
by erdinccurebal · 2026-02-21
90.0%
#20390: fix(daemon): fall back to /tmp for launchd logs on removable volumes
by lemoz · 2026-02-18
80.1%
#18593: fix: resolve symlinks in session path validation (#18553)
by EpaL · 2026-02-16
79.4%
#16929: fix(security): block access to sensitive directories from within sa...
by CornBrother0x · 2026-02-15
78.3%
#22401: fix: resolve relative media paths against workspace and fix /tmp on...
by derrickburns · 2026-02-21
78.2%
#8517: Browser: sandbox download/trace paths
by coygeek · 2026-02-04
77.3%
#7007: Fix security audit false-positive for symlinked state dir
by MohammadErfan-Jabbari · 2026-02-02
76.6%
#8718: fix: sanitize download filenames to prevent path traversal (CWE-22)
by DevZenPro · 2026-02-04
75.9%
#11529: fix(wizard): strip shell-style backslash escapes from workspace paths
by mcaxtr · 2026-02-07
75.8%
#17237: fix(update): guard post-install imports after npm global update
by tdjackey · 2026-02-15
75.6%