Skip to content
This repository was archived by the owner on Jan 20, 2022. It is now read-only.

Commit 3f4d0ab

Browse files
committed
reify: debug crash when extracting into symlink
We should never be doing this. Any time we're extracting contents, it should be into an honest to goodness directory, and not any other kind of thing. Commits to follow will make it impossible to reach this condition without some very deep hacking, but the check is here to make it easier to debug if anything like this is reported again. Related-to: npm/cli#3457
1 parent f2b0cee commit 3f4d0ab

File tree

2 files changed

+62
-0
lines changed

2 files changed

+62
-0
lines changed

lib/arborist/reify.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,16 @@ module.exports = cls => class Reifier extends cls {
614614
await rimraf(node.path)
615615
await this[_symlink](node)
616616
} else {
617+
await debug(async () => {
618+
const st = await lstat(node.path).catch(e => null)
619+
if (st && !st.isDirectory()) {
620+
debug.log('unpacking into a non-directory', node)
621+
throw Object.assign(new Error('ENOTDIR: not a directory'), {
622+
code: 'ENOTDIR',
623+
path: node.path,
624+
})
625+
}
626+
})
617627
await pacote.extract(res, node.path, {
618628
...this.options,
619629
resolved: node.resolved,

test/arborist/reify.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ rimrafMock.sync = (...args) => {
1818
throw new Error('rimraf fail')
1919
}
2020
const fs = require('fs')
21+
const { promisify } = require('util')
22+
const symlink = promisify(fs.symlink)
2123
let failRename = null
2224
let failRenameOnce = null
2325
let failMkdir = null
@@ -77,10 +79,23 @@ const warningTracker = () => {
7779
}
7880
}
7981

82+
const debugLogTracker = () => {
83+
const list = []
84+
mockDebug.log = (...msg) => list.push(msg)
85+
return () => {
86+
mockDebug.log = () => {}
87+
return list
88+
}
89+
}
90+
const mockDebug = Object.assign(fn => fn(), { log: () => {} })
91+
8092
const Arborist = t.mock('../../lib/index.js', {
8193
...mocks,
8294
// need to not mock this one, so we still can swap the process object
8395
'../../lib/signal-handling.js': require('../../lib/signal-handling.js'),
96+
97+
// mock the debug so we can test these even when not enabled
98+
'../../lib/debug.js': mockDebug,
8499
})
85100

86101
const { Node, Link, Shrinkwrap } = Arborist
@@ -2304,3 +2319,40 @@ t.test('node_modules may not be a symlink', async t => {
23042319
t.matchSnapshot(tree)
23052320
t.matchSnapshot(warnings())
23062321
})
2322+
2323+
t.test('never unpack into anything other than a real directory', async t => {
2324+
const kUnpack = Symbol.for('unpackNewModules')
2325+
const unpackNewModules = Arborist.prototype[kUnpack]
2326+
const path = t.testdir({
2327+
'package.json': JSON.stringify({
2328+
dependencies: {
2329+
once: '',
2330+
wrappy: 'file:target',
2331+
},
2332+
}),
2333+
target: {
2334+
donotdeleteme: 'please do not delete this',
2335+
'package.json': JSON.stringify({
2336+
name: 'donotclobberme',
2337+
version: '1.2.3-please-do-not-clobber',
2338+
}),
2339+
},
2340+
})
2341+
const arb = newArb({ path })
2342+
const logs = debugLogTracker()
2343+
const wrappy = resolve(path, 'node_modules/once/node_modules/wrappy')
2344+
arb[kUnpack] = async () => {
2345+
// will have already created it
2346+
realRimraf.sync(wrappy)
2347+
const target = resolve(path, 'target')
2348+
await symlink(target, wrappy, 'junction')
2349+
arb[kUnpack] = unpackNewModules
2350+
return arb[kUnpack]()
2351+
}
2352+
await t.rejects(arb.reify({ path }), {
2353+
message: 'ENOTDIR: not a directory',
2354+
code: 'ENOTDIR',
2355+
path: wrappy,
2356+
})
2357+
t.match(logs(), [['unpacking into a non-directory', { path: wrappy }]])
2358+
})

0 commit comments

Comments
 (0)