Description: prevent path escape using drive-relative paths Author: isaacs <i@izs.me> Origin: upstream, https://github.com/npm/node-tar/commit/875a37e3 Bug: https://github.com/npm/node-tar/security/advisories/GHSA-qq89-hq3f-393p Forwarded: not-needed Reviewed-By: Yadd <yadd@debian.org> Last-Update: 2021-11-11 --- a/lib/strip-absolute-path.js +++ b/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] } --- a/lib/unpack.js +++ b/lib/unpack.js @@ -45,6 +45,8 @@ const GID = Symbol('gid') 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' /* istanbul ignore next */ const neverCalled = () => { @@ -226,7 +228,8 @@ if (!this.preservePaths) { const p = normPath(entry.path) - if (p.split('/').includes('..')) { + const parts = p.split('/') + if (parts.includes('..') || isWindows && /^[a-z]:\.\.$/i.test(parts[0])) { this.warn('TAR_ENTRY_ERROR', `path contains '..'`, { entry, path: p, @@ -234,8 +237,7 @@ 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 @@ -258,6 +260,22 @@ 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 + } + return true }