Skip to content

fix(security): resolve symlinks before path validation to prevent directory traversal#398

Open
sorlen008 wants to merge 2 commits intowonderwhy-er:mainfrom
sorlen008:fix/symlink-directory-traversal
Open

fix(security): resolve symlinks before path validation to prevent directory traversal#398
sorlen008 wants to merge 2 commits intowonderwhy-er:mainfrom
sorlen008:fix/symlink-directory-traversal

Conversation

@sorlen008
Copy link
Copy Markdown
Contributor

@sorlen008 sorlen008 commented Mar 26, 2026

User description

Summary

This fixes a directory traversal vulnerability via symlink bypass in isPathAllowed() / validatePath() — reported in #219 (open for ~7 months now).

The attack is straightforward: create a symlink inside an allowed directory that points to a restricted location, then read/write a file through that symlink. Because the target file doesn't exist yet, fs.realpath() fails with ENOENT and the code falls back to the raw (unresolved) path for the allowlist check. The path looks like it's inside the allowed directory, but the symlink redirects the actual I/O to somewhere else entirely.

Example:

  • Allowed directory: /home/user/projects
  • Attacker creates: /home/user/projects/evil -> /etc/ (symlink)
  • Attacker writes to: /home/user/projects/evil/crontab
  • Old behavior: path passes the allowlist check (starts with /home/user/projects/), file gets written to /etc/crontab
  • New behavior: parent /home/user/projects/evil is resolved to /etc/, reconstructed path /etc/crontab fails the allowlist check

What changed

In validatePath() in src/tools/filesystem.ts: when fs.realpath() fails with ENOENT (path doesn't exist), the fix now resolves the parent directory to follow any symlinks in the path chain. If the parent also doesn't exist (nested new directories), it walks up to the deepest existing ancestor and resolves from there. The resolved path is then used for the allowlist check instead of the raw path.

This is a minimal, targeted fix — about 30 lines added, no API changes, no breaking changes. Existing tests (allowed-directories, directory-creation) all pass. The existing test-symlink-security.js test suite already covers this exact attack scenario and expects the fix.

Test plan

  • npx tsc --noEmit passes
  • test-allowed-directories.js — all 6 tests pass
  • test-directory-creation.js — all tests pass (write-to-nonexistent-path still works)
  • test-symlink-security.js — covers the exact attack vector (requires symlink privileges to run; passes on Linux/macOS)

Closes #219

Summary by CodeRabbit

  • Bug Fixes
    • Improved filesystem path validation and symlink handling when canonical resolution fails. Adds robust fallback that resolves symlink targets or locates the nearest existing parent/ancestor, standardizes which resolved path is used for allowlist checks and subsequent operations, and consistently returns that chosen path—making behavior more reliable for broken symlinks and non-existent targets.

CodeAnt-AI Description

Block symlink-based writes outside allowed folders

What Changed

  • File paths are now checked using their real destination, so a symlink inside an allowed folder can no longer point writes or folder creation at a restricted location.
  • When a new file or nested folder does not exist yet, the path check now follows the nearest existing parent so hidden symlinks in the path are still caught.
  • File operations now use the resolved path after validation, preventing writes from following a misleading symlink target.

Impact

✅ Prevented writes outside allowed directories
✅ Safer file creation in symlinked folders
✅ Fewer directory traversal security bypasses

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Mar 26, 2026

CodeAnt AI is reviewing your PR.


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 26, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

validatePath() now resolves broken symlinks by reading link targets and falling back to the nearest existing ancestor via fs.realpath; it uses the resolved path for allowlist checks, existence checks, and calls validateParentDirectories() when necessary, returning the computed path.

Changes

Cohort / File(s) Summary
Symlink & path resolution
src/tools/filesystem.ts
Refactor validatePath to handle fs.realpath ENOENT by lstat+readlink for broken symlinks, resolve the deepest existing ancestor when needed, set pathForNextCheck = resolvedRealPath ?? absoluteOriginal, use that for fs.stat, allowlist validation, and validateParentDirectories(), and return it unconditionally.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related issues

  • Issue 219: Addresses symlink canonicalization / isPathAllowed bypass; this change's ENOENT symlink and ancestor realpath handling appears to match that objective.

Possibly related PRs

  • PR 321: Makes similar validatePath changes (realpath fallback and using a resolved path), indicating a strong code-level relation.

Suggested labels

size:L

Suggested reviewers

  • serg33v

Poem

🐰 I hopped through links both lost and wide,
Read tangled targets, traced parents' stride,
Rebuilt the road where shadows lay,
Pointed checks the brighter way,
Now broken symlinks have a guide.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main security fix: resolving symlinks before path validation to prevent directory traversal attacks.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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.

@codeant-ai codeant-ai Bot added the size:M This PR changes 30-99 lines, ignoring generated files label Mar 26, 2026
@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Mar 26, 2026

Sequence Diagram

This PR updates path validation to resolve parent or ancestor directories when the target file does not yet exist, ensuring symlinks are followed before checking directory allowlists and preventing traversal through symlinked paths.

sequenceDiagram
    participant Caller
    participant FilesystemTool
    participant OS
    participant SecurityPolicy

    Caller->>FilesystemTool: Request file access with path
    FilesystemTool->>OS: Resolve full real path
    alt Path exists
        OS-->>FilesystemTool: Real path
    else Path missing (ENOENT)
        OS-->>FilesystemTool: ENOENT error
        FilesystemTool->>OS: Resolve parent or ancestor directory
        OS-->>FilesystemTool: Resolved directory path
        FilesystemTool->>FilesystemTool: Rebuild full path from resolved directory
    end
    FilesystemTool->>SecurityPolicy: Check resolved path against allowed directories
    alt Path allowed
        SecurityPolicy-->>FilesystemTool: Allowed
        FilesystemTool-->>Caller: Proceed with file operation
    else Path not allowed
        SecurityPolicy-->>FilesystemTool: Denied
        FilesystemTool-->>Caller: Reject path
    end
Loading

Generated by CodeAnt AI

@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Mar 26, 2026

Nitpicks 🔍

🔒 No security issues identified
⚡ Recommended areas for review

  • Security Gap
    The ancestor-walk logic swallows every resolution error and keeps falling back. If resolution fails for permission or filesystem reasons instead of a simple missing path, the code can degrade into raw-path validation instead of failing closed, which should be reviewed carefully for bypass risk.

@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Mar 26, 2026

CodeAnt AI finished reviewing your PR.

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/tools/filesystem.ts`:
- Around line 206-234: The validation currently computes resolvedRealPath (via
fs.realpath fallback) but continues to return or use absoluteOriginal for
mutations, leaving a TOCTOU gap; update validatePath() to return the
resolvedRealPath (or both resolved and original) and ensure callers (writeFile,
createDirectory, moveFile, writePdf) use the resolved path for the actual
filesystem mutations so the same symlink-resolved path used for allowlist checks
is used for writes/moves/creates; locate references to validatePath(),
writeFile(), createDirectory(), moveFile(), and writePdf and switch their
mutation target to the resolved path returned by validatePath().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cbaef680-85eb-4d0d-8f17-456546bb63a6

📥 Commits

Reviewing files that changed from the base of the PR and between d854870 and 448ae06.

📒 Files selected for processing (1)
  • src/tools/filesystem.ts

Comment thread src/tools/filesystem.ts
@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Mar 29, 2026

CodeAnt AI is running Incremental review


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai Bot added size:M This PR changes 30-99 lines, ignoring generated files and removed size:M This PR changes 30-99 lines, ignoring generated files labels Mar 29, 2026
@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Mar 29, 2026

CodeAnt AI Incremental review completed.

@sorlen008 sorlen008 force-pushed the fix/symlink-directory-traversal branch from 0f9e2a4 to 8021546 Compare April 1, 2026 08:08
@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Apr 1, 2026

CodeAnt AI is running Incremental review


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai Bot added size:M This PR changes 30-99 lines, ignoring generated files and removed size:M This PR changes 30-99 lines, ignoring generated files labels Apr 1, 2026
@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Apr 1, 2026

CodeAnt AI Incremental review completed.

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/tools/filesystem.ts`:
- Around line 211-233: The fallback path in the canonicalization logic (around
resolvedRealPath / absoluteOriginal where fs.realpath(parentDir) is caught) must
first detect if the unresolvable leaf is a symlink: call fs.lstat on the
basename/first unresolvable component and if lstat indicates a symlink, read its
target with fs.readlink, resolve that target to an absolute path and feed that
resolved target back through the same canonicalization (fs.realpath) and
allowlist checks instead of simply joining the unresolved basename; keep the
existing walk-up-to-ancestor loop (current, remaining) as the fallback only when
the leaf is not a symlink or the link target cannot be resolved. Ensure you use
the same error handling and do not bypass the allowlist check when handling
readlink results.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c8086ce9-aa55-4181-bfea-594048040a2b

📥 Commits

Reviewing files that changed from the base of the PR and between 0f9e2a4 and 8021546.

📒 Files selected for processing (1)
  • src/tools/filesystem.ts

Comment thread src/tools/filesystem.ts
sorlen008 added a commit to sorlen008/DesktopCommanderMCP that referenced this pull request Apr 3, 2026
…outside-allowlist gap

When fs.realpath() fails with ENOENT, the previous fallback resolved the parent
directory and rejoined the basename (the symlink name itself). Since fs.writeFile()
follows symlinks, a broken symlink inside an allowed directory could redirect a write
to an arbitrary path outside the allowlist.

Fix: before falling back to parent-directory resolution, use fs.lstat() to detect
whether the unreachable path is a symlink. If it is, read the symlink target with
fs.readlink() and resolve it to an absolute canonical path so that the allowlist check
and all subsequent file operations use the actual destination, not the symlink entry.

Also tighten the ancestor-walk loop to stop on non-ENOENT errors (e.g. EPERM) rather
than silently swallowing them, preventing a degraded-resolution bypass on restricted
directory trees.

Closes coderabbitai review comment on PR wonderwhy-er#398.
@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Apr 3, 2026

CodeAnt AI is running Incremental review


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai Bot added size:M This PR changes 30-99 lines, ignoring generated files and removed size:M This PR changes 30-99 lines, ignoring generated files labels Apr 3, 2026
@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Apr 3, 2026

CodeAnt AI Incremental review completed.

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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/tools/filesystem.ts (1)

224-226: Consider logging swallowed errors for debugging.

The empty catch block silently swallows all errors (including EPERM, EIO, etc.). While the fallback to parent resolution is safe, logging these errors would aid debugging in production environments.

Optional improvement
         } catch (symlinkErr) {
-            // Not a symlink or unreadable; fall through to parent-directory resolution
+            // Not a symlink or unreadable; fall through to parent-directory resolution
+            // Log for debugging but don't fail - parent resolution will handle this
+            const se = symlinkErr as NodeJS.ErrnoException;
+            if (se.code && se.code !== 'ENOENT' && se.code !== 'ENOTDIR') {
+                console.debug(`Symlink check failed for ${absoluteOriginal}: ${se.code}`);
+            }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tools/filesystem.ts` around lines 224 - 226, The empty catch that
currently contains the comment "Not a symlink or unreadable; fall through to
parent-directory resolution" should log the caught error instead of swallowing
it: capture the exception in the catch, emit a debug/warn log via your project's
logger (or console.debug) including the error object and the path being
inspected for symlink resolution so you retain the existing fallback behavior
but surface EPERM/EIO/etc. for troubleshooting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/tools/filesystem.ts`:
- Around line 212-226: The symlink handling in the try block sets
resolvedRealPath from path.resolve(readlink(...)) which does not resolve
symlinks in intermediate components and skips the parent-directory realpath
fallback; change the logic in the lstat/readlink branch (around lstat, readlink,
resolvedRealPath) to call fs.realpath() on the resolved link target and assign
that result to resolvedRealPath (fall back to path.resolve only for forming the
initial candidate), so that any nested symlinks are fully resolved before
running isPathAllowed and before subsequent file operations; ensure errors from
fs.realpath are caught and fall through to the existing parent-directory
resolution logic if realpath fails.

---

Nitpick comments:
In `@src/tools/filesystem.ts`:
- Around line 224-226: The empty catch that currently contains the comment "Not
a symlink or unreadable; fall through to parent-directory resolution" should log
the caught error instead of swallowing it: capture the exception in the catch,
emit a debug/warn log via your project's logger (or console.debug) including the
error object and the path being inspected for symlink resolution so you retain
the existing fallback behavior but surface EPERM/EIO/etc. for troubleshooting.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dc2976cf-68ad-48e9-be76-a95a8eecdde8

📥 Commits

Reviewing files that changed from the base of the PR and between 8021546 and ade51d7.

📒 Files selected for processing (1)
  • src/tools/filesystem.ts

Comment thread src/tools/filesystem.ts Outdated
sorlen008 added a commit to sorlen008/DesktopCommanderMCP that referenced this pull request Apr 5, 2026
…outside-allowlist gap

When fs.realpath() fails with ENOENT, the previous fallback resolved the parent
directory and rejoined the basename (the symlink name itself). Since fs.writeFile()
follows symlinks, a broken symlink inside an allowed directory could redirect a write
to an arbitrary path outside the allowlist.

Fix: before falling back to parent-directory resolution, use fs.lstat() to detect
whether the unreachable path is a symlink. If it is, read the symlink target with
fs.readlink() and resolve it to an absolute canonical path so that the allowlist check
and all subsequent file operations use the actual destination, not the symlink entry.

Also tighten the ancestor-walk loop to stop on non-ENOENT errors (e.g. EPERM) rather
than silently swallowing them, preventing a degraded-resolution bypass on restricted
directory trees.

Closes coderabbitai review comment on PR wonderwhy-er#398.
@sorlen008 sorlen008 force-pushed the fix/symlink-directory-traversal branch from ade51d7 to bd32bbb Compare April 5, 2026 08:30
@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Apr 5, 2026

CodeAnt AI is running Incremental review


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai Bot added size:M This PR changes 30-99 lines, ignoring generated files and removed size:M This PR changes 30-99 lines, ignoring generated files labels Apr 5, 2026
@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Apr 5, 2026

CodeAnt AI Incremental review completed.

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.

♻️ Duplicate comments (1)
src/tools/filesystem.ts (1)

255-265: ⚠️ Potential issue | 🔴 Critical

Resolve the first broken symlink component, not just the leaf.

lstat(absoluteOriginal) only handles the case where the requested path itself is the broken symlink. A path like /allowed/link/subdir with link -> /restricted/missing still reconstructs /allowed/link/subdir, and mkdir(..., { recursive: true }) will follow that symlink and create /restricted/missing/subdir; likewise, readlink() targets are only passed through path.resolve(), so chained symlinks inside the target path can still survive the allowlist check. Please canonicalize the first unresolved symlink component / readlink() target with the same realpath walk before calling isPathAllowed(), and add a regression for both shapes.

In Node.js, does `path.resolve()` resolve symlinks or only normalize lexical path segments? If `fs.promises.mkdir('/allowed/link/subdir', { recursive: true })` is called and `link` is a symlink to `/restricted/missing`, will Node follow the symlink and create `/restricted/missing/subdir`?

Also applies to: 275-305

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tools/filesystem.ts` around lines 255 - 265, The current logic only
lstats the full absoluteOriginal and, on a symlink leaf, resolves its readlink
target but does not canonicalize the first unresolved symlink component or the
readlink target through the same realpath-walking logic before calling
isPathAllowed; update the resolution in the function that sets resolvedRealPath
(the block using fs.lstat, fs.readlink and resolvedRealPath) to iterate path
components from the root, lstat each component, and when encountering a symlink
resolve its readlink, then re-run the canonicalization/walk on the resolved
target (i.e., perform the same realpath walk on the readlink result) so the
first broken/intermediate symlink is resolved to its true absolute path before
any mkdir or isPathAllowed check; ensure the same fix is applied to the later
analogous block (lines ~275-305) and add regression tests that cover both a
broken-symlink leaf (/allowed/link -> /restricted/missing +
/allowed/link/subdir) and an intermediate symlink component
(/allowed/link/subdir where link -> /restricted/missing) to verify mkdir and
allowlist checks use the canonical resolved path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/tools/filesystem.ts`:
- Around line 255-265: The current logic only lstats the full absoluteOriginal
and, on a symlink leaf, resolves its readlink target but does not canonicalize
the first unresolved symlink component or the readlink target through the same
realpath-walking logic before calling isPathAllowed; update the resolution in
the function that sets resolvedRealPath (the block using fs.lstat, fs.readlink
and resolvedRealPath) to iterate path components from the root, lstat each
component, and when encountering a symlink resolve its readlink, then re-run the
canonicalization/walk on the resolved target (i.e., perform the same realpath
walk on the readlink result) so the first broken/intermediate symlink is
resolved to its true absolute path before any mkdir or isPathAllowed check;
ensure the same fix is applied to the later analogous block (lines ~275-305) and
add regression tests that cover both a broken-symlink leaf (/allowed/link ->
/restricted/missing + /allowed/link/subdir) and an intermediate symlink
component (/allowed/link/subdir where link -> /restricted/missing) to verify
mkdir and allowlist checks use the canonical resolved path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e1bd8214-4e14-4f22-90fc-79d3f7ce6e42

📥 Commits

Reviewing files that changed from the base of the PR and between ade51d7 and bd32bbb.

📒 Files selected for processing (1)
  • src/tools/filesystem.ts

@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Apr 7, 2026

CodeAnt AI is running Incremental review


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai Bot added size:M This PR changes 30-99 lines, ignoring generated files and removed size:M This PR changes 30-99 lines, ignoring generated files labels Apr 7, 2026
@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Apr 7, 2026

CodeAnt AI Incremental review completed.

@edgarsskore
Copy link
Copy Markdown
Collaborator

Hello @sorlen008!|
Can you check if the commit is correct, we see a lot of deleted lines on filsystem.ts

sorlen008 and others added 2 commits April 10, 2026 00:06
…ectory traversal

When a file doesn't exist yet (e.g., write operations), fs.realpath() fails with
ENOENT and the code fell back to the unresolved path for the allowedDirectories
check. An attacker could create a symlink inside an allowed directory pointing to
a restricted location, then write a new file through that symlink — the path
appeared to be inside the allowed directory but actually targeted a restricted one.

The fix resolves the parent directory (and walks up to the deepest existing
ancestor if needed) so symlinks anywhere in the path chain are followed to their
real targets before the directory allowlist check runs.

Closes wonderwhy-er#219

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…bypass

Previously, validatePath() resolved symlinks for the allowlist check but
returned the original (unresolved) path. This meant callers like writeFile()
and createDirectory() would still operate on the symlink path, following it
to the restricted target. Now validatePath() consistently returns the
resolved (canonical) path so all subsequent file operations use the real
target path, closing the symlink write bypass vector.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sorlen008 sorlen008 force-pushed the fix/symlink-directory-traversal branch from 38ccae1 to 93c9825 Compare April 9, 2026 20:06
@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Apr 9, 2026

CodeAnt AI is running Incremental review


Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@sorlen008
Copy link
Copy Markdown
Contributor Author

@edgarsskore Sorry about that — those deletions came from a stale automated rebase that added some garbage commits. I just force-pushed a clean version rebased on current main.

The actual change is now what it should have been: +42 / -12 in src/tools/filesystem.ts, just the symlink resolution fix in validatePath(). The big deletions you saw weren't intentional.

Should be ready for review now. Apologies for the noise.

@codeant-ai codeant-ai Bot added size:M This PR changes 30-99 lines, ignoring generated files and removed size:M This PR changes 30-99 lines, ignoring generated files labels Apr 9, 2026
@codeant-ai
Copy link
Copy Markdown
Contributor

codeant-ai Bot commented Apr 9, 2026

CodeAnt AI Incremental review completed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:M This PR changes 30-99 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Security] Directory Traversal via Symbolic Link Bypass Leading to Arbitrary Read/Write

2 participants