Skip to content

fs: resolve symlinks before parent components - #65457

Merged
nodejs-github-bot merged 1 commit into
nodejs:mainfrom
jazelly:fix-60295
Nov 10, 2026
Merged

nodejs-github-bot merged 1 commit into
nodejs:mainfrom
jazelly:fix-60295

Conversation

@jazelly

@jazelly jazelly commented Aug 21, 2026 •

Copy link
Copy Markdown
Member

fs.realpath() and fs.realpathSync() resolved symlink targets with
path.resolve(), which collapses .. before the preceding symlink is
resolved. A link like d -> c/../d was turned back into d, causing
an infinite loop.

Fixed this by resolving .. against the already resolved parent instead.

This matches how native implements realpath:
.. is applied to the already-resolved prefix, and symlink targets are
spliced into the remaining path without lexical normalization.

Fixes: #60295
Signed-off-by: jazelly xzha4350@gmail.com

@nodejs-github-bot nodejs-github-bot added fs Issues and PRs related to file-system APIs and the fs module. needs-ci PRs that need a full CI run. labels Aug 21, 2026
@jazelly
jazelly marked this pull request as ready for review October 4, 2026 21:06
@jazelly

This comment was marked as outdated.

@codecov

codecov Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.68%. Comparing base (128f247) to head (f72d6d1).
⚠️ Report is 28 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #65457      +/-   ##
==========================================
- Coverage   90.69%   90.68%   -0.02%     
==========================================
  Files         797      797              
  Lines      279611   279630      +19     
  Branches    53787    53800      +13     
==========================================
- Hits       253603   253584      -19     
- Misses      16295    16335      +40     
+ Partials     9713     9711       -2     
Files with missing lines Coverage Δ
lib/fs.js 97.20% <100.00%> (+0.03%) ⬆️

... and 37 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@jazelly jazelly added the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Oct 5, 2026
@jazelly

jazelly commented Oct 27, 2026

Copy link
Copy Markdown
Member Author

Hi @nodejs/fs, would appreciate any reviews

@jakecastelli jakecastelli left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this change may introduce a permission-model regression in the JS fs.realpathSync() walk.
Repro (current main, run with --permission --allow-fs-read=):
mkdir -p jail secret && echo x > secret/marker && ln -s "$PWD/secret" jail/link
node --permission --allow-fs-read="$PWD/jail" -p "fs.realpathSync('$PWD/jail/link/..')"

On main this returns $PWD/jail, which is inside the grant. With this PR it returns $PWD, which is outside the grant, and does not throw.

toAbsolutePathPreservingDots() keeps .. in the path so that it is applied after the symlink is resolved. The walk then leaves the granted tree, and the lstat/stat/readlink calls made during the walk are not checked against the grant. The permission check on the input string sees the lexical path, which is inside the grant, so it passes.

Expected behaviour: ERR_ACCESS_DENIED, which is what the async fs.realpath() returns on the current main.

Suggestion: check each base against the fs.read grant before it is lstat'd, stat'd, or readlink'd during the walk, and add a regression test under test/parallel/ that covers link/.. with --permission.

@jazelly

jazelly commented Nov 6, 2026 •

Copy link
Copy Markdown
Member Author

@jakecastelli Thanks for the review!

I can see there are 2 concerns here

  1. the permission model when it comes to allow-fs-read with the symlink.
  2. correct behaviour for this case, $PWD/jail vs $PWD

For 1, I think this is a known imperfection.

On the current main line, with --allow-fs-read=jail granted, fs.realpathSync('jail/link/marker') already returns $PWD/secret/marker, which is outside the grant.

As for 2. $PWD/jail vs $PWD, this is actually what I am trying to align in this PR.

  • the realpathSync.native gives the same result as $PWD
  • and the promises.realpath

The js version, realpathSync is currently doing wrong on main, as it operates on string directly by offsetting jail/link/.. to jail/. However, the correct logic should be

  1. lstat the symlink on $PWD/jail/link first, and found $PWD/secret/
  2. do the .., which goes to parent $PWD

So $PWD should be the correct one.

Hope that makes sense.

@jakecastelli jakecastelli left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

yes, my bad, it should be "$PWD", I made a mistake

@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Nov 7, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

The JavaScript implementation of `fs.realpath()` and
`fs.realpathSync()` joined a symlink target with the rest of the path
through `path.resolve()`, which collapses `..` lexically before the
preceding components have been resolved. A link such as
`d -> c/../d`, where `c` is itself a symlink, was therefore rewritten
back to `d` and the walk never terminated.

Keep `.` and `..` in the path while walking it, and apply `..` to the
already resolved parent, matching `realpath(3)` and
`fs.realpathSync.native()`. Only normalize the final result, and skip
caching link targets containing `..`, whose resolution depends on the
path walked before them.

Thsi is a POSIX only fix. Windows is unaffected.

Signed-off-by: jazelly <xzha4350@gmail.com>
@jazelly
jazelly requested a review from jakecastelli November 8, 2026 10:29
@jazelly jazelly added the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Nov 8, 2026

@jakecastelli jakecastelli left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Nov 8, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@jazelly jazelly added the request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. label Nov 9, 2026
@github-actions github-actions Bot added request-ci-failed Starting CI with the request-ci label failed and requires manual intervention. and removed request-ci Add this label to start a Jenkins CI on a PR. Only starts once the PR has an approving review. labels Nov 9, 2026
@github-actions

This comment was marked as outdated.

@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@jazelly jazelly added commit-queue PRs queued for automated landing through the Commit Queue. and removed request-ci-failed Starting CI with the request-ci label failed and requires manual intervention. labels Nov 10, 2026
@nodejs-github-bot
nodejs-github-bot merged commit 40aad9e into nodejs:main Nov 10, 2026
68 checks passed
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

Landed in 40aad9e

@nodejs-github-bot nodejs-github-bot removed the commit-queue PRs queued for automated landing through the Commit Queue. label Nov 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fs Issues and PRs related to file-system APIs and the fs module. needs-ci PRs that need a full CI run.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fs.realpath infinite loop

3 participants