Skip to content

Commit 01c0ca6

Browse files
Fix copy/move recursing into source through a symlinked dest ancestor (#1073)
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.
1 parent 7268250 commit 01c0ca6

3 files changed

Lines changed: 58 additions & 2 deletions

File tree

lib/copy/__tests__/copy-prevent-copying-into-itself.test.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,29 @@ describe('+ copy() - prevent copying into itself', () => {
210210
})
211211
})
212212

213+
it('should error when dest is a subdirectory of src and the dest parent does not exist yet', done => {
214+
const destLink = path.join(TEST_DIR, 'dest-symlink')
215+
fs.symlinkSync(src, destLink, 'dir')
216+
217+
const srclenBefore = klawSync(src).length
218+
assert(srclenBefore > 2)
219+
220+
// The immediate dest parent ('sub') does not exist yet, but a deeper
221+
// ancestor ('dest-symlink') is a symlink into src.
222+
const dest = path.join(destLink, 'sub', 'dest')
223+
assert(!fs.existsSync(path.dirname(dest)))
224+
fs.copy(src, dest, err => {
225+
assert.strictEqual(err.message, `Cannot copy '${src}' to a subdirectory of itself, '${path.join(destLink, 'sub')}'.`)
226+
227+
const srclenAfter = klawSync(src).length
228+
assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change')
229+
230+
const link = fs.readlinkSync(destLink)
231+
assert.strictEqual(link, src)
232+
done()
233+
})
234+
})
235+
213236
it('should copy the directory successfully when src is a subdir of resolved dest path and dereference is true', done => {
214237
const srcInDest = path.join(TEST_DIR, 'dest', 'some', 'nested', 'src')
215238
const destLink = path.join(TEST_DIR, 'dest-symlink')

lib/copy/__tests__/copy-sync-prevent-copying-into-itself.test.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,33 @@ describe('+ copySync() - prevent copying into itself', () => {
216216
}
217217
})
218218

219+
it('should error when dest is a subdirectory of src and the dest parent does not exist yet', () => {
220+
const destLink = path.join(TEST_DIR, 'dest-symlink')
221+
fs.symlinkSync(src, destLink, 'dir')
222+
223+
const srclenBefore = klawSync(src).length
224+
assert(srclenBefore > 2)
225+
226+
// The immediate dest parent ('sub') does not exist yet, but a deeper
227+
// ancestor ('dest-symlink') is a symlink into src.
228+
const dest = path.join(destLink, 'sub', 'dest')
229+
assert(!fs.existsSync(path.dirname(dest)))
230+
let errThrown = false
231+
try {
232+
fs.copySync(src, dest)
233+
} catch (err) {
234+
assert.strictEqual(err.message, `Cannot copy '${src}' to a subdirectory of itself, '${path.join(destLink, 'sub')}'.`)
235+
errThrown = true
236+
} finally {
237+
assert(errThrown)
238+
const srclenAfter = klawSync(src).length
239+
assert.strictEqual(srclenAfter, srclenBefore, 'src length should not change')
240+
241+
const link = fs.readlinkSync(destLink)
242+
assert.strictEqual(link, src)
243+
}
244+
})
245+
219246
it('should copy the directory successfully when src is a subdir of resolved dest path and dereferene is true', () => {
220247
const srcInDest = path.join(TEST_DIR, 'dest', 'some', 'nested', 'src')
221248
const destLink = path.join(TEST_DIR, 'dest-symlink')

lib/util/stat.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,10 @@ async function checkParentPaths (src, srcStat, dest, funcName) {
101101
try {
102102
destStat = await fs.stat(destParent, { bigint: true })
103103
} catch (err) {
104-
if (err.code === 'ENOENT') return
104+
// The destination parent does not exist yet, but a deeper ancestor might
105+
// (e.g. when it is a symlink into the source tree). Keep walking up so the
106+
// self-subdirectory check is not bypassed.
107+
if (err.code === 'ENOENT') return checkParentPaths(src, srcStat, destParent, funcName)
105108
throw err
106109
}
107110

@@ -120,7 +123,10 @@ function checkParentPathsSync (src, srcStat, dest, funcName) {
120123
try {
121124
destStat = fs.statSync(destParent, { bigint: true })
122125
} catch (err) {
123-
if (err.code === 'ENOENT') return
126+
// The destination parent does not exist yet, but a deeper ancestor might
127+
// (e.g. when it is a symlink into the source tree). Keep walking up so the
128+
// self-subdirectory check is not bypassed.
129+
if (err.code === 'ENOENT') return checkParentPathsSync(src, srcStat, destParent, funcName)
124130
throw err
125131
}
126132
if (areIdentical(srcStat, destStat)) {

0 commit comments

Comments
 (0)