Skip to content

Fix copy/move recursing into source through a symlinked destination ancestor#1073

Merged
RyanZim merged 1 commit into
jprichardson:masterfrom
sarathfrancis90:fix-copy-symlink-ancestor-recursion
Jun 29, 2026
Merged

Fix copy/move recursing into source through a symlinked destination ancestor#1073
RyanZim merged 1 commit into
jprichardson:masterfrom
sarathfrancis90:fix-copy-symlink-ancestor-recursion

Conversation

@sarathfrancis90

Copy link
Copy Markdown
Contributor

Fixes #1071.

checkParentPaths walks up the destination's ancestors to catch the case where the destination is actually inside the source, which otherwise makes copy recurse into the very tree it is creating. When an intermediate destination parent doesn't exist yet, it returned early on ENOENT and stopped checking. If a deeper ancestor is a symlink into the source tree, that early return bypasses the check entirely.

The result is that copy(src, link/sub/dest) (where link -> src and link/sub doesn't exist yet) keeps copying src into itself, growing src/sub/dest/sub/dest/... until it dies with ENAMETOOLONG, instead of raising the usual "Cannot copy to a subdirectory of itself" error.

Per @RyanZim's suggestion in the issue, the fix recurses to the next parent on ENOENT instead of returning, so the check keeps walking up until it reaches an existing ancestor (or the source parent / root). This affects copy, copySync, move, and moveSync since they all go through checkParentPaths.

Testing

  • Added regression tests for copy and copySync covering a non-existent dest parent under a symlink-into-src ancestor (fail before this change: async times out from the runaway recursion, sync doesn't throw; both pass after).
  • Verified legitimate deep copies into multiple non-existent parents still succeed (no false positives).
  • Full suite green: npm test (lint + 728 unit + ESM).

checkParentPaths walks up the destination's ancestors to detect when the
destination is inside the source (which would cause copy to recurse into
the tree it is creating). When an intermediate parent did not exist yet
it returned early on ENOENT, so a deeper ancestor that is a symlink into
the source tree was never checked. copy then created the missing path
under the symlink target and walked into the subtree until ENAMETOOLONG.

Recurse to the next parent on ENOENT instead of returning, so the
self-subdirectory check keeps walking up to an existing ancestor.

@JPeer264 JPeer264 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Implementation and tests look fine, but I'm not the best to judge.

@JPeer264

Copy link
Copy Markdown
Collaborator

Also thanks a lot for taking your time to fix it

@RyanZim RyanZim merged commit 01c0ca6 into jprichardson:master Jun 29, 2026
21 checks passed
@RyanZim

RyanZim commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Published in fs-extra@11.3.6; sorry for the delay.

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.

copy/copySync can recurse into the source tree through a symlinked destination ancestor

3 participants