Skip to content

fix(tool): check write permissions against symlink-resolved paths - #621

Open
blueberrycongee wants to merge 1 commit into
OpenBMB:mainfrom
blueberrycongee:fix/write-symlink-escape
Open

blueberrycongee wants to merge 1 commit into
OpenBMB:mainfrom
blueberrycongee:fix/write-symlink-escape

Conversation

@blueberrycongee

Copy link
Copy Markdown
Contributor

Summary

write_file / edit_file checked write permission against the literal path, while the OS follows symlinks when it writes. The symlink-resolved check in resolvePilotDeckWorkspacePath only ran in the mustExist branch, which these tools don't use. So a symlink inside the workspace could send a write somewhere the checks never looked at:

  • repo-internals -> .git: write_file .git/HEAD is denied, but write_file repo-internals/HEAD passed and overwrote .git/HEAD (after that, git symbolic-ref HEAD exits 128). This also worked in bypassPermissions mode, which is still supposed to enforce the .git / node_modules / dist deny list.
  • escape -> /some/outside/dir: checkFilesystemWritePermission returned passthrough instead of ask, so the write left the workspace with no prompt.
  • Same result for a file symlink (head-link -> .git/HEAD), a dangling file symlink (config-link -> .git/config), and edit_file.

Changes

  • pathSafety.ts: add resolveRealWritePath. It finds where a write will actually land by resolving the deepest existing ancestor and then following any dangling symlinks, so it works for files that don't exist yet. For writes, the deny list is now checked against both the literal path and this resolved path. If a literal in-workspace path resolves outside every workspace root, the check now reports it as outside the workspace. That means checkFilesystemWritePermission returns ask instead of passthrough, and execute without an allow decision rejects the write.
  • matchPermissionRule.ts: an allow rule for write_file / edit_file with no pattern only covers in-workspace paths. It now also requires the symlink-resolved path to be inside the workspace, so a symlink escape can't slip through an existing rule without a prompt.
  • Symlinks that resolve to a location inside the workspace still work as before. The returned absolutePath / relativePath are unchanged, so read-before-write snapshots and diffs behave the same.

Remaining limitation: this is a check-then-write sequence. A symlink swapped in between the check and the write (TOCTOU) is not covered; that would need OS-level sandboxing.

Validation

  • New tests/tool/write-symlink-escape.spec.ts (8 cases). On upstream/main without the fix, 6 of the 7 tool-level cases fail. The one that passes is the in-workspace symlink control case. With the fix, all 8 pass.
  • Full compiled suite with the fix: 663 passed, 0 failed, 0 cancelled, 2 skipped (665 total).
  • Same suite on a clean upstream/main (e76ae087) worktree: 655 passed, 0 failed, 0 cancelled, 2 skipped (657 total).
  • Note: run on Node 24.20.0 by calling tsc + the plugin copy step directly, because npm run build's runtime guard requires Node 22 and no Node 22 was available locally. No native-module tests were affected in either run.

🤖 Generated with Claude Code

write_file and edit_file only checked the literal path the caller
supplied, while the OS follows symlinks when writing. A workspace
symlink such as `repo-internals -> .git` let writes bypass the default
.git/node_modules/dist deny list, and a symlink to a directory outside
the workspace let writes skip the outside-workspace permission prompt.

Resolve the real write target (including not-yet-existing files and
dangling symlinks) and apply the deny list and workspace boundary to it
as well. Workspace-scoped write_file/edit_file allow rules also no
longer match paths that resolve outside the workspace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant