Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nodejs/node.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'deps/npm/node_modules/tar/lib/unpack.js')
-rw-r--r--deps/npm/node_modules/tar/lib/unpack.js379
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)