Skip to content

fix(security): follow symlinks in workspace boundary check (#169)#241

Open
proyectoauraorg wants to merge 4 commits into
Zoo-Code-Org:mainfrom
proyectoauraorg:fix/169-symlink-workspace-boundary
Open

fix(security): follow symlinks in workspace boundary check (#169)#241
proyectoauraorg wants to merge 4 commits into
Zoo-Code-Org:mainfrom
proyectoauraorg:fix/169-symlink-workspace-boundary

Conversation

@proyectoauraorg
Copy link
Copy Markdown
Contributor

@proyectoauraorg proyectoauraorg commented May 21, 2026

Summary

Fixes #169 (security). The out-of-workspace read protection could be bypassed with a symlink: isPathOutsideWorkspace() only resolved ./.. (path.resolve), not symlink targets. A symlink that lives inside the workspace but points outside it was therefore treated as inside, so it slipped past the boundary check.

Fix

Resolve the real path (following symlinks via fs.realpathSync) for both the target path and the workspace folders before comparing. A symlink inside the workspace that points outside now correctly resolves outside and is flagged.

For paths that don't exist yet (e.g. a file about to be created), the realpath of the nearest existing ancestor is resolved and the remaining segments re-appended, so creation flows still work and any symlinked ancestor is still followed.

Testing

vitest run utils/__tests__/pathUtils.spec.ts
# Test Files 1 passed (1) · Tests 6 passed (6)

New tests use real symlinks in a temp dir: a symlinked file and a symlinked directory inside the workspace that point outside are both flagged as outside; real in-workspace files (existing and not-yet-created) stay inside; real outside files stay outside.

Summary by CodeRabbit

  • Bug Fixes

    • Workspace boundary detection now resolves symbolic links so files or directories inside the workspace that point outside are classified as external.
  • New Features

    • New context setting to optionally allow symlinks that point outside the workspace to be treated as inside (off by default), with UI wiring and localized strings.
  • Tests

    • Added tests covering symlinks, non-existent files, permission errors, and no-workspace scenarios.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 21, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3e3376cb-3213-4bc5-bb35-0774131021cc

📥 Commits

Reviewing files that changed from the base of the PR and between 7652419 and 19cc696.

📒 Files selected for processing (1)
  • .changeset/symlink-workspace-boundary.md
✅ Files skipped from review due to trivial changes (1)
  • .changeset/symlink-workspace-boundary.md

📝 Walkthrough

Walkthrough

Resolves symlink bypass by canonicalizing paths and updating workspace containment checks (fail-closed). Adds tests, a new allowSymlinksOutsideWorkspace setting (default false) with types/UI/i18n wiring, and updates core tools to use the async workspace-check helper.

Changes

Symlink-aware path containment validation

Layer / File(s) Summary
Changeset and settings schema
.changeset/symlink-workspace-boundary.md, packages/types/src/global-settings.ts, packages/types/src/vscode-extension-host.ts
Adds a changeset entry and a new allowSymlinksOutsideWorkspace boolean setting (validated in schema, default false) and exposes it in ExtensionState.
Path canonicalization helpers with symlink resolution
src/utils/pathUtils.ts
Adds fs-based helpers (isErrnoException, realPathOrNearest) to canonicalize paths via fs.realpathSync.native, walking up on ENOENT and re-throwing non-ENOENT errors.
Updated isPathOutsideWorkspace to use canonical paths
src/utils/pathUtils.ts
Refactors isPathOutsideWorkspace to resolve the target and workspace folders with realPathOrNearest by default (fail-closed) and to accept allowSymlinksOutsideWorkspace to fall back to lexical path.resolve.
Test suite for isPathOutsideWorkspace
src/utils/__tests__/pathUtils.spec.ts
Adds tests with mocked vscode.workspace, temp filesystem fixtures, and assertions for real inside/outside files, non-existent inside paths, symlinked file/directory resolving outside, option-enabled lexical behavior, fail-closed error paths, and no-workspace behavior.
BaseTool helper and tools integration
src/core/tools/BaseTool.ts, src/core/tools/EditFileTool.ts, src/core/tools/ListFilesTool.ts, src/core/tools/SearchReplaceTool.ts
Adds BaseTool.resolveIsOutsideWorkspace which reads provider state for allowSymlinksOutsideWorkspace and delegates to isPathOutsideWorkspace; updates EditFileTool, ListFilesTool, and SearchReplaceTool to await the async resolver and removes direct imports of the old sync helper.
Settings UI wiring and i18n
webview-ui/src/components/settings/ContextManagementSettings.tsx, webview-ui/src/components/settings/SettingsView.tsx, webview-ui/src/i18n/locales/*/settings.json
Wires the new setting into cached UI state, submit payloads, renders a checkbox in context management settings, and adds localized label/description strings across locales.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Suggested reviewers

  • hannesrudolph
  • edelauna

Poem

🐰
I nibbled paths through roots and leaves,
Found secret tunnels no one perceives.
Now I tidy links with a careful hop,
No more slips past the workspace top. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the main change: fixing a security bug where symlinks in workspace boundary checks are now followed to prevent bypassing protections.
Description check ✅ Passed The PR description covers key implementation details (realpath resolution, handling non-existent files, opt-in setting) and includes testing results, but the structured template sections (Related GitHub Issue, Pre-Submission Checklist) are not formally filled.
Linked Issues check ✅ Passed The PR fully implements the requirements from issue #169: resolves symlinks in workspace boundary checks, adds necessary security fixes, and includes the requested opt-in allowSymlinksOutsideWorkspace setting with UI integration.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #169: symlink resolution logic, the new opt-in setting, UI integration, internationalization strings, and comprehensive tests. No unrelated changes detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 21, 2026

Codecov Report

❌ Patch coverage is 86.11111% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/utils/pathUtils.ts 86.11% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@nh2
Copy link
Copy Markdown

nh2 commented May 21, 2026

@proyectoauraorg Thank you! I think this needs to be optional via the

  • Include symlinks in workspace resolving to outside of workspace

setting suggested in #169. There will be inevitably be users whose workflow will be broken without such a setting.

Comment thread src/utils/pathUtils.ts Outdated
try {
const resolved = fs.realpathSync.native(current)
return trailing.length > 0 ? path.join(resolved, ...trailing.reverse()) : resolved
} catch {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if realpathSync.native throws something other than ENOENT — e.g. EACCES on a symlink whose target has restricted permissions? The walk-up would skip the symlink entirely and return the lexical path, so isPathOutsideWorkspace would report "inside workspace" even though the real target might be outside.

For a security boundary, would it be safer to catch only ENOENT and let other errors propagate (or treat them as "outside")? Something like:

Suggested change
} catch {
} catch (err: unknown) {
if (!(err instanceof Error && 'code' in err && (err as NodeJS.ErrnoException).code === 'ENOENT')) {
// Non-ENOENT error (e.g. EACCES) — fail closed for safety.
return path.resolve(target)
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch — thanks @edelauna 🙏

Fixed in 49a70d9: the walk-up now only happens on ENOENT. Any other error (e.g. EACCES on a symlink whose target has restricted permissions) propagates, and isPathOutsideWorkspace catches it and fails closed — treating the path as outside rather than falling back to the lexical path.

I went with propagate-and-fail-closed instead of returning path.resolve(target) directly, because returning the lexical path would still report a workspace-internal symlink as "inside" in exactly the case you flagged. Added a regression test that stubs realpath to throw EACCES.

Happy to adjust if you'd prefer a different shape.

@proyectoauraorg
Copy link
Copy Markdown
Contributor Author

Thanks @nh2 — that's a fair point. Symlinks pointing outside the workspace are a legitimate workflow for some users, and a hard block shouldn't break them silently.

The opt-in setting from #169 ("Include symlinks resolving outside of workspace") is the right way to cover that. My suggestion would be to land this PR as the default-safe security fix and add the setting as a focused follow-up, so the config + UI + i18n changes get their own review — but I'm equally happy to add it here if a single PR is preferred.

@edelauna any preference on scope?

@edelauna
Copy link
Copy Markdown
Contributor

@proyectoauraorg lets track the ui/setting as a sub-issue. I'll wait to approve this PR until we also have the settings so that we can push both in the same release.

proyectoauraorg added a commit to proyectoauraorg/Zoo-Code that referenced this pull request May 22, 2026
…-Code-Org#169)

Adds an opt-in `allowSymlinksOutsideWorkspace` setting (default off) so users who deliberately rely on symlinks pointing outside the workspace can bypass the fail-closed boundary check from Zoo-Code-Org#169/Zoo-Code-Org#241. When enabled, isPathOutsideWorkspace compares lexical paths instead of resolving symlinks. Threaded to the read/list/edit tools via a BaseTool helper. UI + i18n follow.
proyectoauraorg added a commit to proyectoauraorg/Zoo-Code that referenced this pull request May 22, 2026
…Org#246)

Surfaces the opt-in setting in the Context settings panel (toggle, default off) and adds its label + description across all 18 locales. Pairs with the workspace-boundary symlink fix so both ship together (Zoo-Code-Org#169 / Zoo-Code-Org#241).
@proyectoauraorg
Copy link
Copy Markdown
Contributor Author

Added the opt-in setting (#246) to this PR so the fix and the escape hatch ship together, as discussed.

  • New allowSymlinksOutsideWorkspace setting (default off — preserves the fail-closed [BUG] vscode out-of-workspace read protection trivially circumvented by symlinks #169 behavior). When enabled, a symlink inside the workspace that points outside is treated by its lexical location instead of being resolved and flagged.
  • Surfaced as a toggle in the Context settings panel, with label + description across all 18 locales.
  • Threaded through isPathOutsideWorkspace via a BaseTool helper, so the list/read/edit tools all honor it. Added a regression test for the opt-in path.

tsc + unit tests are green (pathUtils, the three tool suites, and the SettingsView change-detection suite). Ready for another look 🙏

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
webview-ui/src/components/settings/SettingsView.tsx (1)

406-407: ⚡ Quick win

Add local webview tests for the new symlink setting wiring.

Please add/extend Vitest coverage for this setting’s dirty-state and save payload path (including init vs user-edit behavior).

As per coding guidelines, webview-ui/src/**/*.{ts,tsx}: “Prefer local webview-ui tests… Add or update Vitest coverage under webview-ui/src/**/__tests__”, and webview-ui/src/**/SettingsView.{ts,tsx}: “tests should distinguish automatic initialization from real user edits.”

Also applies to: 840-840

🤖 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 `@webview-ui/src/components/settings/SettingsView.tsx` around lines 406 - 407,
Add Vitest unit tests under webview-ui/src/**/__tests__ that cover the new
allowSymlinksOutsideWorkspace wiring in the SettingsView component: verify
initial initialization (when SettingsView reads default or persisted settings)
does not mark the form as dirty and does not include unintended values in the
save payload, and verify a real user edit to the allowSymlinksOutsideWorkspace
toggle marks the view as dirty and produces the expected save payload (including
allowSymlinksOutsideWorkspace and maxImageFileSize fields). Target the
SettingsView component (and any local helpers used to build the save payload)
and write assertions for initial vs user-edited behavior, dirty-state changes,
and that the saved payload contains the correct boolean/number values for
allowSymlinksOutsideWorkspace and maxImageFileSize.
🤖 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.

Nitpick comments:
In `@webview-ui/src/components/settings/SettingsView.tsx`:
- Around line 406-407: Add Vitest unit tests under webview-ui/src/**/__tests__
that cover the new allowSymlinksOutsideWorkspace wiring in the SettingsView
component: verify initial initialization (when SettingsView reads default or
persisted settings) does not mark the form as dirty and does not include
unintended values in the save payload, and verify a real user edit to the
allowSymlinksOutsideWorkspace toggle marks the view as dirty and produces the
expected save payload (including allowSymlinksOutsideWorkspace and
maxImageFileSize fields). Target the SettingsView component (and any local
helpers used to build the save payload) and write assertions for initial vs
user-edited behavior, dirty-state changes, and that the saved payload contains
the correct boolean/number values for allowSymlinksOutsideWorkspace and
maxImageFileSize.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ff706651-a819-4254-9e23-9915d643fbf3

📥 Commits

Reviewing files that changed from the base of the PR and between 166bc3f and 7652419.

📒 Files selected for processing (29)
  • .changeset/symlink-workspace-boundary.md
  • packages/types/src/global-settings.ts
  • packages/types/src/vscode-extension-host.ts
  • src/core/tools/BaseTool.ts
  • src/core/tools/EditFileTool.ts
  • src/core/tools/ListFilesTool.ts
  • src/core/tools/SearchReplaceTool.ts
  • src/utils/__tests__/pathUtils.spec.ts
  • src/utils/pathUtils.ts
  • webview-ui/src/components/settings/ContextManagementSettings.tsx
  • webview-ui/src/components/settings/SettingsView.tsx
  • webview-ui/src/i18n/locales/ca/settings.json
  • webview-ui/src/i18n/locales/de/settings.json
  • webview-ui/src/i18n/locales/en/settings.json
  • webview-ui/src/i18n/locales/es/settings.json
  • webview-ui/src/i18n/locales/fr/settings.json
  • webview-ui/src/i18n/locales/hi/settings.json
  • webview-ui/src/i18n/locales/id/settings.json
  • webview-ui/src/i18n/locales/it/settings.json
  • webview-ui/src/i18n/locales/ja/settings.json
  • webview-ui/src/i18n/locales/ko/settings.json
  • webview-ui/src/i18n/locales/nl/settings.json
  • webview-ui/src/i18n/locales/pl/settings.json
  • webview-ui/src/i18n/locales/pt-BR/settings.json
  • webview-ui/src/i18n/locales/ru/settings.json
  • webview-ui/src/i18n/locales/tr/settings.json
  • webview-ui/src/i18n/locales/vi/settings.json
  • webview-ui/src/i18n/locales/zh-CN/settings.json
  • webview-ui/src/i18n/locales/zh-TW/settings.json

proyectoauraorg and others added 4 commits May 22, 2026 11:09
…-Org#169)

isPathOutsideWorkspace() only normalized ./.. so a symlink living inside the
workspace but pointing outside passed the check, trivially bypassing the
out-of-workspace read protection. Resolve the real path (following symlinks)
for both the target and the workspace folders before comparing. Paths that
don't exist yet resolve via their nearest existing ancestor.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…g#169)

Per @edelauna's review: only ENOENT triggers the nearest-ancestor walk-up.
Any other error (e.g. EACCES on a symlink whose target has restricted
permissions) now propagates, and isPathOutsideWorkspace fails closed —
treating the path as outside instead of masking the symlink with the lexical
path. Adds a regression test that stubs realpath to throw EACCES.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…-Code-Org#169)

Adds an opt-in `allowSymlinksOutsideWorkspace` setting (default off) so users who deliberately rely on symlinks pointing outside the workspace can bypass the fail-closed boundary check from Zoo-Code-Org#169/Zoo-Code-Org#241. When enabled, isPathOutsideWorkspace compares lexical paths instead of resolving symlinks. Threaded to the read/list/edit tools via a BaseTool helper. UI + i18n follow.
…Org#246)

Surfaces the opt-in setting in the Context settings panel (toggle, default off) and adds its label + description across all 18 locales. Pairs with the workspace-boundary symlink fix so both ship together (Zoo-Code-Org#169 / Zoo-Code-Org#241).
@proyectoauraorg proyectoauraorg force-pushed the fix/169-symlink-workspace-boundary branch from 7652419 to 19cc696 Compare May 22, 2026 17:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] vscode out-of-workspace read protection trivially circumvented by symlinks

3 participants