diff options
Diffstat (limited to 'deps/npm/node_modules/tar/lib/unpack.js')
-rw-r--r-- | deps/npm/node_modules/tar/lib/unpack.js | 379 |
1 files changed, 277 insertions, 102 deletions
diff --git a/deps/npm/node_modules/tar/lib/unpack.js b/deps/npm/node_modules/tar/lib/unpack.js index edaf7833cdb..7f397f10379 100644 --- a/deps/npm/node_modules/tar/lib/unpack.js +++ b/deps/npm/node_modules/tar/lib/unpack.js @@ -15,10 +15,13 @@ const mkdir = require('./mkdir.js') const wc = require('./winchars.js') const pathReservations = require('./path-reservations.js') const stripAbsolutePath = require('./strip-absolute-path.js') +const normPath = require('./normalize-windows-path.js') +const stripSlash = require('./strip-trailing-slashes.js') const ONENTRY = Symbol('onEntry') const CHECKFS = Symbol('checkFs') const CHECKFS2 = Symbol('checkFs2') +const PRUNECACHE = Symbol('pruneCache') const ISREUSABLE = Symbol('isReusable') const MAKEFS = Symbol('makeFs') const FILE = Symbol('file') @@ -39,13 +42,11 @@ const SKIP = Symbol('skip') const DOCHOWN = Symbol('doChown') const UID = Symbol('uid') const GID = Symbol('gid') +const CHECKED_CWD = Symbol('checkedCwd') const crypto = require('crypto') const getFlag = require('./get-write-flag.js') - -/* istanbul ignore next */ -const neverCalled = () => { - throw new Error('sync function called cb somehow?!?') -} +const platform = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform +const isWindows = platform === 'win32' // Unlinks on Windows are not atomic. // @@ -64,7 +65,7 @@ const neverCalled = () => { // See: https://github.com/npm/node-tar/issues/183 /* istanbul ignore next */ const unlinkFile = (path, cb) => { - if (process.platform !== 'win32') + if (!isWindows) return fs.unlink(path, cb) const name = path + '.DELETE.' + crypto.randomBytes(16).toString('hex') @@ -77,7 +78,7 @@ const unlinkFile = (path, cb) => { /* istanbul ignore next */ const unlinkFileSync = path => { - if (process.platform !== 'win32') + if (!isWindows) return fs.unlinkSync(path) const name = path + '.DELETE.' + crypto.randomBytes(16).toString('hex') @@ -91,6 +92,33 @@ const uint32 = (a, b, c) => : b === b >>> 0 ? b : c +// clear the cache if it's a case-insensitive unicode-squashing match. +// we can't know if the current file system is case-sensitive or supports +// unicode fully, so we check for similarity on the maximally compatible +// representation. Err on the side of pruning, since all it's doing is +// preventing lstats, and it's not the end of the world if we get a false +// positive. +// Note that on windows, we always drop the entire cache whenever a +// symbolic link is encountered, because 8.3 filenames are impossible +// to reason about, and collisions are hazards rather than just failures. +const cacheKeyNormalize = path => stripSlash(normPath(path)) + .normalize('NFKD') + .toLowerCase() + +const pruneCache = (cache, abs) => { + abs = cacheKeyNormalize(abs) + for (const path of cache.keys()) { + const pnorm = cacheKeyNormalize(path) + if (pnorm === abs || pnorm.indexOf(abs + '/') === 0) + cache.delete(path) + } +} + +const dropCache = cache => { + for (const key of cache.keys()) + cache.delete(key) +} + class Unpack extends Parser { constructor (opt) { if (!opt) @@ -103,6 +131,8 @@ class Unpack extends Parser { super(opt) + this[CHECKED_CWD] = false + this.reservations = pathReservations() this.transform = typeof opt.transform === 'function' ? opt.transform : null @@ -148,7 +178,7 @@ class Unpack extends Parser { this.forceChown = opt.forceChown === true // turn ><?| in filenames into 0xf000-higher encoded forms - this.win32 = !!opt.win32 || process.platform === 'win32' + this.win32 = !!opt.win32 || isWindows // do not unpack over files that are newer than what's in the archive this.newer = !!opt.newer @@ -168,7 +198,7 @@ class Unpack extends Parser { // links, and removes symlink directories rather than erroring this.unlink = !!opt.unlink - this.cwd = path.resolve(opt.cwd || process.cwd()) + this.cwd = normPath(path.resolve(opt.cwd || process.cwd())) this.strip = +opt.strip || 0 // if we're not chmodding, then we don't need the process umask this.processUmask = opt.noChmod ? 0 : process.umask() @@ -201,21 +231,24 @@ class Unpack extends Parser { [CHECKPATH] (entry) { if (this.strip) { - const parts = entry.path.split(/\/|\\/) + const parts = normPath(entry.path).split('/') if (parts.length < this.strip) return false entry.path = parts.slice(this.strip).join('/') if (entry.type === 'Link') { - const linkparts = entry.linkpath.split(/\/|\\/) + const linkparts = normPath(entry.linkpath).split('/') if (linkparts.length >= this.strip) entry.linkpath = linkparts.slice(this.strip).join('/') + else + return false } } if (!this.preservePaths) { - const p = entry.path - if (p.match(/(^|\/|\\)\.\.(\\|\/|$)/)) { + const p = normPath(entry.path) + const parts = p.split('/') + if (parts.includes('..') || isWindows && /^[a-z]:\.\.$/i.test(parts[0])) { this.warn('TAR_ENTRY_ERROR', `path contains '..'`, { entry, path: p, @@ -223,8 +256,7 @@ class Unpack extends Parser { return false } - // absolutes on posix are also absolutes on win32 - // so we only need to test this one to get both + // strip off the root const [root, stripped] = stripAbsolutePath(p) if (root) { entry.path = stripped @@ -235,18 +267,42 @@ class Unpack extends Parser { } } + if (path.isAbsolute(entry.path)) + entry.absolute = normPath(path.resolve(entry.path)) + else + entry.absolute = normPath(path.resolve(this.cwd, entry.path)) + + // if we somehow ended up with a path that escapes the cwd, and we are + // not in preservePaths mode, then something is fishy! This should have + // been prevented above, so ignore this for coverage. + /* istanbul ignore if - defense in depth */ + if (!this.preservePaths && + entry.absolute.indexOf(this.cwd + '/') !== 0 && + entry.absolute !== this.cwd) { + this.warn('TAR_ENTRY_ERROR', 'path escaped extraction target', { + entry, + path: normPath(entry.path), + resolvedPath: entry.absolute, + cwd: this.cwd, + }) + return false + } + + // an archive can set properties on the extraction directory, but it + // may not replace the cwd with a different kind of thing entirely. + if (entry.absolute === this.cwd && + entry.type !== 'Directory' && + entry.type !== 'GNUDumpDir') + return false + // only encode : chars that aren't drive letter indicators if (this.win32) { - const parsed = path.win32.parse(entry.path) - entry.path = parsed.root === '' ? wc.encode(entry.path) - : parsed.root + wc.encode(entry.path.substr(parsed.root.length)) + const { root: aRoot } = path.win32.parse(entry.absolute) + entry.absolute = aRoot + wc.encode(entry.absolute.substr(aRoot.length)) + const { root: pRoot } = path.win32.parse(entry.path) + entry.path = pRoot + wc.encode(entry.path.substr(pRoot.length)) } - if (path.isAbsolute(entry.path)) - entry.absolute = entry.path - else - entry.absolute = path.resolve(this.cwd, entry.path) - return true } @@ -291,7 +347,7 @@ class Unpack extends Parser { } [MKDIR] (dir, mode, cb) { - mkdir(dir, { + mkdir(normPath(dir), { uid: this.uid, gid: this.gid, processUid: this.processUid, @@ -333,17 +389,37 @@ class Unpack extends Parser { mode: mode, autoClose: false, }) - stream.on('error', er => this[ONERROR](er, entry)) + stream.on('error', er => { + if (stream.fd) + fs.close(stream.fd, () => {}) + + // flush all the data out so that we aren't left hanging + // if the error wasn't actually fatal. otherwise the parse + // is blocked, and we never proceed. + stream.write = () => true + this[ONERROR](er, entry) + fullyDone() + }) let actions = 1 const done = er => { - if (er) - return this[ONERROR](er, entry) + if (er) { + /* istanbul ignore else - we should always have a fd by now */ + if (stream.fd) + fs.close(stream.fd, () => {}) + + this[ONERROR](er, entry) + fullyDone() + return + } if (--actions === 0) { fs.close(stream.fd, er => { + if (er) + this[ONERROR](er, entry) + else + this[UNPEND]() fullyDone() - er ? this[ONERROR](er, entry) : this[UNPEND]() }) } } @@ -378,7 +454,10 @@ class Unpack extends Parser { const tx = this.transform ? this.transform(entry) || entry : entry if (tx !== entry) { - tx.on('error', er => this[ONERROR](er, entry)) + tx.on('error', er => { + this[ONERROR](er, entry) + fullyDone() + }) entry.pipe(tx) } tx.pipe(stream) @@ -388,8 +467,9 @@ class Unpack extends Parser { const mode = entry.mode & 0o7777 || this.dmode this[MKDIR](entry.absolute, mode, er => { if (er) { + this[ONERROR](er, entry) fullyDone() - return this[ONERROR](er, entry) + return } let actions = 1 @@ -427,7 +507,8 @@ class Unpack extends Parser { } [HARDLINK] (entry, done) { - this[LINK](entry, path.resolve(this.cwd, entry.linkpath), 'link', done) + const linkpath = normPath(path.resolve(this.cwd, entry.linkpath)) + this[LINK](entry, linkpath, 'link', done) } [PEND] () { @@ -452,7 +533,7 @@ class Unpack extends Parser { !this.unlink && st.isFile() && st.nlink <= 1 && - process.platform !== 'win32' + !isWindows } // check if a thing is there, and if so, try to clobber it @@ -464,51 +545,115 @@ class Unpack extends Parser { this.reservations.reserve(paths, done => this[CHECKFS2](entry, done)) } - [CHECKFS2] (entry, done) { + [PRUNECACHE] (entry) { // if we are not creating a directory, and the path is in the dirCache, // then that means we are about to delete the directory we created // previously, and it is no longer going to be a directory, and neither // is any of its children. - if (entry.type !== 'Directory') { - for (const path of this.dirCache.keys()) { - if (path === entry.absolute || - path.indexOf(entry.absolute + '/') === 0 || - path.indexOf(entry.absolute + '\\') === 0) - this.dirCache.delete(path) - } + // If a symbolic link is encountered, all bets are off. There is no + // reasonable way to sanitize the cache in such a way we will be able to + // avoid having filesystem collisions. If this happens with a non-symlink + // entry, it'll just fail to unpack, but a symlink to a directory, using an + // 8.3 shortname or certain unicode attacks, can evade detection and lead + // to arbitrary writes to anywhere on the system. + if (entry.type === 'SymbolicLink') + dropCache(this.dirCache) + else if (entry.type !== 'Directory') + pruneCache(this.dirCache, entry.absolute) + } + + [CHECKFS2] (entry, fullyDone) { + this[PRUNECACHE](entry) + + const done = er => { + this[PRUNECACHE](entry) + fullyDone(er) } - this[MKDIR](path.dirname(entry.absolute), this.dmode, er => { - if (er) { - done() - return this[ONERROR](er, entry) + const checkCwd = () => { + this[MKDIR](this.cwd, this.dmode, er => { + if (er) { + this[ONERROR](er, entry) + done() + return + } + this[CHECKED_CWD] = true + start() + }) + } + + const start = () => { + if (entry.absolute !== this.cwd) { + const parent = normPath(path.dirname(entry.absolute)) + if (parent !== this.cwd) { + return this[MKDIR](parent, this.dmode, er => { + if (er) { + this[ONERROR](er, entry) + done() + return + } + afterMakeParent() + }) + } } - fs.lstat(entry.absolute, (er, st) => { + afterMakeParent() + } + + const afterMakeParent = () => { + fs.lstat(entry.absolute, (lstatEr, st) => { if (st && (this.keep || this.newer && st.mtime > entry.mtime)) { this[SKIP](entry) done() - } else if (er || this[ISREUSABLE](entry, st)) - this[MAKEFS](null, entry, done) + return + } + if (lstatEr || this[ISREUSABLE](entry, st)) + return this[MAKEFS](null, entry, done) - else if (st.isDirectory()) { + if (st.isDirectory()) { if (entry.type === 'Directory') { - if (!this.noChmod && (!entry.mode || (st.mode & 0o7777) === entry.mode)) - this[MAKEFS](null, entry, done) - else { - fs.chmod(entry.absolute, entry.mode, - er => this[MAKEFS](er, entry, done)) - } - } else - fs.rmdir(entry.absolute, er => this[MAKEFS](er, entry, done)) - } else - unlinkFile(entry.absolute, er => this[MAKEFS](er, entry, done)) + const needChmod = !this.noChmod && + entry.mode && + (st.mode & 0o7777) !== entry.mode + const afterChmod = er => this[MAKEFS](er, entry, done) + if (!needChmod) + return afterChmod() + return fs.chmod(entry.absolute, entry.mode, afterChmod) + } + // Not a dir entry, have to remove it. + // NB: the only way to end up with an entry that is the cwd + // itself, in such a way that == does not detect, is a + // tricky windows absolute path with UNC or 8.3 parts (and + // preservePaths:true, or else it will have been stripped). + // In that case, the user has opted out of path protections + // explicitly, so if they blow away the cwd, c'est la vie. + if (entry.absolute !== this.cwd) { + return fs.rmdir(entry.absolute, er => + this[MAKEFS](er, entry, done)) + } + } + + // not a dir, and not reusable + // don't remove if the cwd, we want that error + if (entry.absolute === this.cwd) + return this[MAKEFS](null, entry, done) + + unlinkFile(entry.absolute, er => + this[MAKEFS](er, entry, done)) }) - }) + } + + if (this[CHECKED_CWD]) + start() + else + checkCwd() } [MAKEFS] (er, entry, done) { - if (er) - return this[ONERROR](er, entry) + if (er) { + this[ONERROR](er, entry) + done() + return + } switch (entry.type) { case 'File': @@ -529,58 +674,82 @@ class Unpack extends Parser { } [LINK] (entry, linkpath, link, done) { - // XXX: get the type ('file' or 'dir') for windows + // XXX: get the type ('symlink' or 'junction') for windows fs[link](linkpath, entry.absolute, er => { if (er) - return this[ONERROR](er, entry) + this[ONERROR](er, entry) + else { + this[UNPEND]() + entry.resume() + } done() - this[UNPEND]() - entry.resume() }) } } +const callSync = fn => { + try { + return [null, fn()] + } catch (er) { + return [er, null] + } +} class UnpackSync extends Unpack { + [MAKEFS] (er, entry) { + return super[MAKEFS](er, entry, () => {}) + } + [CHECKFS] (entry) { - if (entry.type !== 'Directory') { - for (const path of this.dirCache.keys()) { - if (path === entry.absolute || - path.indexOf(entry.absolute + '/') === 0 || - path.indexOf(entry.absolute + '\\') === 0) - this.dirCache.delete(path) + this[PRUNECACHE](entry) + + if (!this[CHECKED_CWD]) { + const er = this[MKDIR](this.cwd, this.dmode) + if (er) + return this[ONERROR](er, entry) + this[CHECKED_CWD] = true + } + + // don't bother to make the parent if the current entry is the cwd, + // we've already checked it. + if (entry.absolute !== this.cwd) { + const parent = normPath(path.dirname(entry.absolute)) + if (parent !== this.cwd) { + const mkParent = this[MKDIR](parent, this.dmode) + if (mkParent) + return this[ONERROR](mkParent, entry) } } - const er = this[MKDIR](path.dirname(entry.absolute), this.dmode, neverCalled) - if (er) - return this[ONERROR](er, entry) - try { - const st = fs.lstatSync(entry.absolute) - if (this.keep || this.newer && st.mtime > entry.mtime) - return this[SKIP](entry) - else if (this[ISREUSABLE](entry, st)) - return this[MAKEFS](null, entry, neverCalled) - else { - try { - if (st.isDirectory()) { - if (entry.type === 'Directory') { - if (!this.noChmod && entry.mode && (st.mode & 0o7777) !== entry.mode) - fs.chmodSync(entry.absolute, entry.mode) - } else - fs.rmdirSync(entry.absolute) - } else - unlinkFileSync(entry.absolute) - return this[MAKEFS](null, entry, neverCalled) - } catch (er) { - return this[ONERROR](er, entry) - } + const [lstatEr, st] = callSync(() => fs.lstatSync(entry.absolute)) + if (st && (this.keep || this.newer && st.mtime > entry.mtime)) + return this[SKIP](entry) + + if (lstatEr || this[ISREUSABLE](entry, st)) + return this[MAKEFS](null, entry) + + if (st.isDirectory()) { + if (entry.type === 'Directory') { + const needChmod = !this.noChmod && + entry.mode && + (st.mode & 0o7777) !== entry.mode + const [er] = needChmod ? callSync(() => { + fs.chmodSync(entry.absolute, entry.mode) + }) : [] + return this[MAKEFS](er, entry) } - } catch (er) { - return this[MAKEFS](null, entry, neverCalled) + // not a dir entry, have to remove it + const [er] = callSync(() => fs.rmdirSync(entry.absolute)) + this[MAKEFS](er, entry) } + + // not a dir, and not reusable. + // don't remove if it's the cwd, since we want that error. + const [er] = entry.absolute === this.cwd ? [] + : callSync(() => unlinkFileSync(entry.absolute)) + this[MAKEFS](er, entry) } - [FILE] (entry, _) { + [FILE] (entry, done) { const mode = entry.mode & 0o7777 || this.fmode const oner = er => { @@ -592,6 +761,7 @@ class UnpackSync extends Unpack { } if (er || closeError) this[ONERROR](er || closeError, entry) + done() } let fd @@ -651,11 +821,14 @@ class UnpackSync extends Unpack { }) } - [DIRECTORY] (entry, _) { + [DIRECTORY] (entry, done) { const mode = entry.mode & 0o7777 || this.dmode const er = this[MKDIR](entry.absolute, mode) - if (er) - return this[ONERROR](er, entry) + if (er) { + this[ONERROR](er, entry) + done() + return + } if (entry.mtime && !this.noMtime) { try { fs.utimesSync(entry.absolute, entry.atime || new Date(), entry.mtime) @@ -666,12 +839,13 @@ class UnpackSync extends Unpack { fs.chownSync(entry.absolute, this[UID](entry), this[GID](entry)) } catch (er) {} } + done() entry.resume() } [MKDIR] (dir, mode) { try { - return mkdir.sync(dir, { + return mkdir.sync(normPath(dir), { uid: this.uid, gid: this.gid, processUid: this.processUid, @@ -688,9 +862,10 @@ class UnpackSync extends Unpack { } } - [LINK] (entry, linkpath, link, _) { + [LINK] (entry, linkpath, link, done) { try { fs[link + 'Sync'](linkpath, entry.absolute) + done() entry.resume() } catch (er) { return this[ONERROR](er, entry) |