Skip to content

fix(security): resolve path traversal bypass in evaluateFilePath#288

Open
pbbadenhorst wants to merge 1 commit intomksglu:nextfrom
pbbadenhorst:fix/execute-file-path-traversal
Open

fix(security): resolve path traversal bypass in evaluateFilePath#288
pbbadenhorst wants to merge 1 commit intomksglu:nextfrom
pbbadenhorst:fix/execute-file-path-traversal

Conversation

@pbbadenhorst
Copy link
Copy Markdown

@pbbadenhorst pbbadenhorst commented Apr 15, 2026

What / Why / How

What: evaluateFilePath (and therefore checkFilePathDenyPolicy for ctx_execute_file / Read-routed tools) now matches deny globs against both the raw input string and the fully-resolved absolute path.

Why: Read deny patterns were matched only against the raw input. An absolute-path deny rule like Read(/Users/you/.ssh/**) could be bypassed by passing a relative path with .. segments that resolves into the same location — e.g. ../../.ssh/id_rsa from a project nested under $HOME. A prompt-injected ctx_execute_file call could then read SSH keys, AWS creds, .env files, etc. that the user explicitly tried to deny.

How:

  • evaluateFilePath (src/security.ts) gains an optional 4th parameter projectRoot. When supplied, the path is also resolved via path.resolve(projectRoot, filePath) and the resolved form is matched against every deny glob in addition to the raw input.
  • checkFilePathDenyPolicy (src/server.ts) passes process.env.CLAUDE_PROJECT_DIR ?? process.cwd() so the resolved form is checked at runtime.
  • Backwards compatible: callers that don't pass projectRoot see identical behavior. A regression-guard test documents that pre-fix behavior so any future change is intentional.

Affected platforms

  • All platforms

(The fix is in shared security/server code that all adapters route through.)

Test plan

Added two tests to tests/security.test.ts in the existing evaluateFilePath describe block:

  1. traversal does not bypass absolute deny glob when projectRoot is supplied — the bypass case. With projectRoot = ~/some/project and deny glob ~/.ssh/**, asserts that ../../.ssh/id_rsa is denied. Fails before the fix, passes after.
  2. without projectRoot, absolute deny glob is still bypassable (regression guard) — pins the legacy 3-arg behavior so future changes to it are deliberate.

Also verified:

  • npm test → 1540 passed / 17 skipped / 0 failed (46 files)
  • npm run typecheck → clean

Checklist

  • Tests added/updated (TDD: red → green confirmed locally)
  • npm test passes
  • npm run typecheck passes
  • Docs updated if needed (no doc surface changed — internal function gained an optional param)
  • No Windows path regressions (forward-slash normalization preserved; path.resolve is platform-aware)
  • Targets next branch

Read deny patterns were matched only against the raw input string, so
absolute-path globs like Read(/Users/you/.ssh/**) could be bypassed by
passing a relative path with `..` segments that resolved into the same
location (e.g. ../../.ssh/id_rsa from a project nested under $HOME).

evaluateFilePath now accepts an optional projectRoot and matches deny
globs against both the raw input and the fully-resolved absolute path.
checkFilePathDenyPolicy in server.ts passes CLAUDE_PROJECT_DIR (falling
back to process.cwd()) so the resolved form is checked for ctx_execute_file
and any other tool routed through the policy.

Backwards compatible: callers that don't pass projectRoot see identical
behavior — covered by a regression-guard test alongside the new bypass test.
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.

2 participants