From b6f40b5f85094387f2fa8d42b6a624644b8ddcf1 Mon Sep 17 00:00:00 2001 From: Gar Date: Thu, 19 Aug 2021 07:32:28 -0700 Subject: [PATCH] tar@6.1.10 * fix: prune dirCache properly for unicode, windows * fix: reserve paths properly for unicode, windows * fix: prevent path escape using drive-relative paths * fix: drop dirCache for symlink on all platforms --- node_modules/tar/lib/path-reservations.js | 31 +++++- node_modules/tar/lib/strip-absolute-path.js | 14 ++- node_modules/tar/lib/unpack.js | 108 ++++++++++++++++---- node_modules/tar/package.json | 2 +- package-lock.json | 14 +-- package.json | 2 +- 6 files changed, 133 insertions(+), 38 deletions(-) diff --git a/node_modules/tar/lib/path-reservations.js b/node_modules/tar/lib/path-reservations.js index 167447af08057..8d0ead9b6017f 100644 --- a/node_modules/tar/lib/path-reservations.js +++ b/node_modules/tar/lib/path-reservations.js @@ -8,8 +8,12 @@ const assert = require('assert') const normPath = require('./normalize-windows-path.js') +const stripSlashes = require('./strip-trailing-slashes.js') const { join } = require('path') +const platform = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform +const isWindows = platform === 'win32' + module.exports = () => { // path => [function or Set] // A Set object means a directory reservation @@ -20,10 +24,16 @@ module.exports = () => { const reservations = new Map() // return a set of parent dirs for a given path - const getDirs = path => - path.split('/').slice(0, -1).reduce((set, path) => - set.length ? set.concat(normPath(join(set[set.length - 1], path))) - : [path], []) + // '/a/b/c/d' -> ['/', '/a', '/a/b', '/a/b/c', '/a/b/c/d'] + const getDirs = path => { + const dirs = path.split('/').slice(0, -1).reduce((set, path) => { + if (set.length) + path = normPath(join(set[set.length - 1], path)) + set.push(path || '/') + return set + }, []) + return dirs + } // functions currently running const running = new Set() @@ -99,7 +109,18 @@ module.exports = () => { } const reserve = (paths, fn) => { - paths = paths.map(p => normPath(join(p)).toLowerCase()) + // collide on matches across case and unicode normalization + // On windows, thanks to the magic of 8.3 shortnames, it is fundamentally + // impossible to determine whether two paths refer to the same thing on + // disk, without asking the kernel for a shortname. + // So, we just pretend that every path matches every other path here, + // effectively removing all parallelization on windows. + paths = isWindows ? ['win32 parallelization disabled'] : paths.map(p => { + return stripSlashes(normPath(join(p))) + .normalize('NFKD') + .toLowerCase() + }) + const dirs = new Set( paths.map(path => getDirs(path)).reduce((a, b) => a.concat(b)) ) diff --git a/node_modules/tar/lib/strip-absolute-path.js b/node_modules/tar/lib/strip-absolute-path.js index 49161ddc30473..1aa2d2aec5030 100644 --- a/node_modules/tar/lib/strip-absolute-path.js +++ b/node_modules/tar/lib/strip-absolute-path.js @@ -2,13 +2,23 @@ const { isAbsolute, parse } = require('path').win32 // returns [root, stripped] +// Note that windows will think that //x/y/z/a has a "root" of //x/y, and in +// those cases, we want to sanitize it to x/y/z/a, not z/a, so we strip / +// explicitly if it's the first character. +// drive-specific relative paths on Windows get their root stripped off even +// though they are not absolute, so `c:../foo` becomes ['c:', '../foo'] module.exports = path => { let r = '' - while (isAbsolute(path)) { + + let parsed = parse(path) + while (isAbsolute(path) || parsed.root) { // windows will think that //x/y/z has a "root" of //x/y/ - const root = path.charAt(0) === '/' ? '/' : parse(path).root + // but strip the //?/C:/ off of //?/C:/path + const root = path.charAt(0) === '/' && path.slice(0, 4) !== '//?/' ? '/' + : parsed.root path = path.substr(root.length) r += root + parsed = parse(path) } return [r, path] } diff --git a/node_modules/tar/lib/unpack.js b/node_modules/tar/lib/unpack.js index cf10d07347b69..7f397f1037921 100644 --- a/node_modules/tar/lib/unpack.js +++ b/node_modules/tar/lib/unpack.js @@ -16,10 +16,12 @@ 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') @@ -43,6 +45,8 @@ const GID = Symbol('gid') const CHECKED_CWD = Symbol('checkedCwd') const crypto = require('crypto') const getFlag = require('./get-write-flag.js') +const platform = process.env.TESTING_TAR_FAKE_PLATFORM || process.platform +const isWindows = platform === 'win32' // Unlinks on Windows are not atomic. // @@ -61,7 +65,7 @@ const getFlag = require('./get-write-flag.js') // 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') @@ -74,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') @@ -88,17 +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) => { - // clear the cache if it's a case-insensitive match, since we can't - // know if the current file system is case-sensitive or not. - abs = normPath(abs).toLowerCase() + abs = cacheKeyNormalize(abs) for (const path of cache.keys()) { - const plower = path.toLowerCase() - if (plower === abs || plower.toLowerCase().indexOf(abs + '/') === 0) + 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) @@ -158,7 +178,7 @@ class Unpack extends Parser { this.forceChown = opt.forceChown === true // turn > 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') + // 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) + } const checkCwd = () => { this[MKDIR](this.cwd, this.dmode, er => { @@ -566,7 +619,13 @@ class Unpack extends Parser { return afterChmod() return fs.chmod(entry.absolute, entry.mode, afterChmod) } - // not a dir entry, have to remove it. + // 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)) @@ -641,8 +700,7 @@ class UnpackSync extends Unpack { } [CHECKFS] (entry) { - if (entry.type !== 'Directory') - pruneCache(this.dirCache, entry.absolute) + this[PRUNECACHE](entry) if (!this[CHECKED_CWD]) { const er = this[MKDIR](this.cwd, this.dmode) @@ -691,7 +749,7 @@ class UnpackSync extends Unpack { this[MAKEFS](er, entry) } - [FILE] (entry, _) { + [FILE] (entry, done) { const mode = entry.mode & 0o7777 || this.fmode const oner = er => { @@ -703,6 +761,7 @@ class UnpackSync extends Unpack { } if (er || closeError) this[ONERROR](er || closeError, entry) + done() } let fd @@ -762,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) @@ -777,6 +839,7 @@ class UnpackSync extends Unpack { fs.chownSync(entry.absolute, this[UID](entry), this[GID](entry)) } catch (er) {} } + done() entry.resume() } @@ -799,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) diff --git a/node_modules/tar/package.json b/node_modules/tar/package.json index 42f55d0356ca4..a10cdac85ea8f 100644 --- a/node_modules/tar/package.json +++ b/node_modules/tar/package.json @@ -2,7 +2,7 @@ "author": "Isaac Z. Schlueter (http://blog.izs.me/)", "name": "tar", "description": "tar for node", - "version": "6.1.8", + "version": "6.1.10", "repository": { "type": "git", "url": "https://github.com/npm/node-tar.git" diff --git a/package-lock.json b/package-lock.json index e55c9732d35c6..a2e0da86f1f6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -144,7 +144,7 @@ "rimraf": "^3.0.2", "semver": "^7.3.5", "ssri": "^8.0.1", - "tar": "^6.1.8", + "tar": "^6.1.10", "text-table": "~0.2.0", "tiny-relative-date": "^1.3.0", "treeverse": "^1.0.4", @@ -9446,9 +9446,9 @@ } }, "node_modules/tar": { - "version": "6.1.8", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.8.tgz", - "integrity": "sha512-sb9b0cp855NbkMJcskdSYA7b11Q8JsX4qe4pyUAfHp+Y6jBjJeek2ZVlwEfWayshEIwlIzXx0Fain3QG9JPm2A==", + "version": "6.1.10", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.10.tgz", + "integrity": "sha512-kvvfiVvjGMxeUNB6MyYv5z7vhfFRwbwCXJAeL0/lnbrttBVqcMOnpHUf0X42LrPMR8mMpgapkJMchFH4FSHzNA==", "inBundle": true, "dependencies": { "chownr": "^2.0.0", @@ -17381,9 +17381,9 @@ } }, "tar": { - "version": "6.1.8", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.8.tgz", - "integrity": "sha512-sb9b0cp855NbkMJcskdSYA7b11Q8JsX4qe4pyUAfHp+Y6jBjJeek2ZVlwEfWayshEIwlIzXx0Fain3QG9JPm2A==", + "version": "6.1.10", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.10.tgz", + "integrity": "sha512-kvvfiVvjGMxeUNB6MyYv5z7vhfFRwbwCXJAeL0/lnbrttBVqcMOnpHUf0X42LrPMR8mMpgapkJMchFH4FSHzNA==", "requires": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", diff --git a/package.json b/package.json index f6db0adab20c0..7478b7d558752 100644 --- a/package.json +++ b/package.json @@ -114,7 +114,7 @@ "rimraf": "^3.0.2", "semver": "^7.3.5", "ssri": "^8.0.1", - "tar": "^6.1.8", + "tar": "^6.1.10", "text-table": "~0.2.0", "tiny-relative-date": "^1.3.0", "treeverse": "^1.0.4",