@@ -18,10 +18,12 @@ const wc = require('./winchars.js')
1818const pathReservations = require ( './path-reservations.js' )
1919const stripAbsolutePath = require ( './strip-absolute-path.js' )
2020const normPath = require ( './normalize-windows-path.js' )
21+ const stripSlash = require ( './strip-trailing-slashes.js' )
2122
2223const ONENTRY = Symbol ( 'onEntry' )
2324const CHECKFS = Symbol ( 'checkFs' )
2425const CHECKFS2 = Symbol ( 'checkFs2' )
26+ const PRUNECACHE = Symbol ( 'pruneCache' )
2527const ISREUSABLE = Symbol ( 'isReusable' )
2628const MAKEFS = Symbol ( 'makeFs' )
2729const FILE = Symbol ( 'file' )
@@ -46,6 +48,8 @@ const GID = Symbol('gid')
4648const CHECKED_CWD = Symbol ( 'checkedCwd' )
4749const crypto = require ( 'crypto' )
4850const getFlag = require ( './get-write-flag.js' )
51+ const platform = process . env . TESTING_TAR_FAKE_PLATFORM || process . platform
52+ const isWindows = platform === 'win32'
4953
5054// Unlinks on Windows are not atomic.
5155//
@@ -64,7 +68,7 @@ const getFlag = require('./get-write-flag.js')
6468// See: https://github.com/npm/node-tar/issues/183
6569/* istanbul ignore next */
6670const unlinkFile = ( path , cb ) => {
67- if ( process . platform !== 'win32' )
71+ if ( ! isWindows )
6872 return fs . unlink ( path , cb )
6973
7074 const name = path + '.DELETE.' + crypto . randomBytes ( 16 ) . toString ( 'hex' )
@@ -77,7 +81,7 @@ const unlinkFile = (path, cb) => {
7781
7882/* istanbul ignore next */
7983const unlinkFileSync = path => {
80- if ( process . platform !== 'win32' )
84+ if ( ! isWindows )
8185 return fs . unlinkSync ( path )
8286
8387 const name = path + '.DELETE.' + crypto . randomBytes ( 16 ) . toString ( 'hex' )
@@ -91,17 +95,33 @@ const uint32 = (a, b, c) =>
9195 : b === b >>> 0 ? b
9296 : c
9397
98+ // clear the cache if it's a case-insensitive unicode-squashing match.
99+ // we can't know if the current file system is case-sensitive or supports
100+ // unicode fully, so we check for similarity on the maximally compatible
101+ // representation. Err on the side of pruning, since all it's doing is
102+ // preventing lstats, and it's not the end of the world if we get a false
103+ // positive.
104+ // Note that on windows, we always drop the entire cache whenever a
105+ // symbolic link is encountered, because 8.3 filenames are impossible
106+ // to reason about, and collisions are hazards rather than just failures.
107+ const cacheKeyNormalize = path => stripSlash ( normPath ( path ) )
108+ . normalize ( 'NFKD' )
109+ . toLowerCase ( )
110+
94111const pruneCache = ( cache , abs ) => {
95- // clear the cache if it's a case-insensitive match, since we can't
96- // know if the current file system is case-sensitive or not.
97- abs = normPath ( abs ) . toLowerCase ( )
112+ abs = cacheKeyNormalize ( abs )
98113 for ( const path of cache . keys ( ) ) {
99- const plower = path . toLowerCase ( )
100- if ( plower === abs || plower . toLowerCase ( ) . indexOf ( abs + '/' ) === 0 )
114+ const pnorm = cacheKeyNormalize ( path )
115+ if ( pnorm === abs || pnorm . indexOf ( abs + '/' ) === 0 )
101116 cache . delete ( path )
102117 }
103118}
104119
120+ const dropCache = cache => {
121+ for ( const key of cache . keys ( ) )
122+ cache . delete ( key )
123+ }
124+
105125class Unpack extends Parser {
106126 constructor ( opt ) {
107127 if ( ! opt )
@@ -160,7 +180,7 @@ class Unpack extends Parser {
160180 this . forceChown = opt . forceChown === true
161181
162182 // turn ><?| in filenames into 0xf000-higher encoded forms
163- this . win32 = ! ! opt . win32 || process . platform === 'win32'
183+ this . win32 = ! ! opt . win32 || isWindows
164184
165185 // do not unpack over files that are newer than what's in the archive
166186 this . newer = ! ! opt . newer
@@ -494,7 +514,7 @@ class Unpack extends Parser {
494514 ! this . unlink &&
495515 st . isFile ( ) &&
496516 st . nlink <= 1 &&
497- process . platform !== 'win32'
517+ ! isWindows
498518 }
499519
500520 // check if a thing is there, and if so, try to clobber it
@@ -505,13 +525,31 @@ class Unpack extends Parser {
505525 paths . push ( entry . linkpath )
506526 this . reservations . reserve ( paths , done => this [ CHECKFS2 ] ( entry , done ) )
507527 }
508- [ CHECKFS2 ] ( entry , done ) {
528+
529+ [ PRUNECACHE ] ( entry ) {
509530 // if we are not creating a directory, and the path is in the dirCache,
510531 // then that means we are about to delete the directory we created
511532 // previously, and it is no longer going to be a directory, and neither
512533 // is any of its children.
513- if ( entry . type !== 'Directory' )
534+ // If a symbolic link is encountered on Windows, all bets are off.
535+ // There is no reasonable way to sanitize the cache in such a way
536+ // we will be able to avoid having filesystem collisions. If this
537+ // happens with a non-symlink entry, it'll just fail to unpack,
538+ // but a symlink to a directory, using an 8.3 shortname, can evade
539+ // detection and lead to arbitrary writes to anywhere on the system.
540+ if ( isWindows && entry . type === 'SymbolicLink' )
541+ dropCache ( this . dirCache )
542+ else if ( entry . type !== 'Directory' )
514543 pruneCache ( this . dirCache , entry . absolute )
544+ }
545+
546+ [ CHECKFS2 ] ( entry , fullyDone ) {
547+ this [ PRUNECACHE ] ( entry )
548+
549+ const done = er => {
550+ this [ PRUNECACHE ] ( entry )
551+ fullyDone ( er )
552+ }
515553
516554 const checkCwd = ( ) => {
517555 this [ MKDIR ] ( this . cwd , this . dmode , er => {
@@ -562,7 +600,13 @@ class Unpack extends Parser {
562600 return afterChmod ( )
563601 return fs . chmod ( entry . absolute , entry . mode , afterChmod )
564602 }
565- // not a dir entry, have to remove it.
603+ // Not a dir entry, have to remove it.
604+ // NB: the only way to end up with an entry that is the cwd
605+ // itself, in such a way that == does not detect, is a
606+ // tricky windows absolute path with UNC or 8.3 parts (and
607+ // preservePaths:true, or else it will have been stripped).
608+ // In that case, the user has opted out of path protections
609+ // explicitly, so if they blow away the cwd, c'est la vie.
566610 if ( entry . absolute !== this . cwd ) {
567611 return fs . rmdir ( entry . absolute , er =>
568612 this [ MAKEFS ] ( er , entry , done ) )
@@ -637,8 +681,7 @@ class UnpackSync extends Unpack {
637681 }
638682
639683 [ CHECKFS ] ( entry ) {
640- if ( entry . type !== 'Directory' )
641- pruneCache ( this . dirCache , entry . absolute )
684+ this [ PRUNECACHE ] ( entry )
642685
643686 if ( ! this [ CHECKED_CWD ] ) {
644687 const er = this [ MKDIR ] ( this . cwd , this . dmode )
@@ -687,7 +730,7 @@ class UnpackSync extends Unpack {
687730 this [ MAKEFS ] ( er , entry )
688731 }
689732
690- [ FILE ] ( entry , _ ) {
733+ [ FILE ] ( entry , done ) {
691734 const mode = entry . mode & 0o7777 || this . fmode
692735
693736 const oner = er => {
@@ -699,6 +742,7 @@ class UnpackSync extends Unpack {
699742 }
700743 if ( er || closeError )
701744 this [ ONERROR ] ( er || closeError , entry )
745+ done ( )
702746 }
703747
704748 let stream
@@ -759,11 +803,14 @@ class UnpackSync extends Unpack {
759803 } )
760804 }
761805
762- [ DIRECTORY ] ( entry , _ ) {
806+ [ DIRECTORY ] ( entry , done ) {
763807 const mode = entry . mode & 0o7777 || this . dmode
764808 const er = this [ MKDIR ] ( entry . absolute , mode )
765- if ( er )
766- return this [ ONERROR ] ( er , entry )
809+ if ( er ) {
810+ this [ ONERROR ] ( er , entry )
811+ done ( )
812+ return
813+ }
767814 if ( entry . mtime && ! this . noMtime ) {
768815 try {
769816 fs . utimesSync ( entry . absolute , entry . atime || new Date ( ) , entry . mtime )
@@ -774,6 +821,7 @@ class UnpackSync extends Unpack {
774821 fs . chownSync ( entry . absolute , this [ UID ] ( entry ) , this [ GID ] ( entry ) )
775822 } catch ( er ) { }
776823 }
824+ done ( )
777825 entry . resume ( )
778826 }
779827
@@ -796,9 +844,10 @@ class UnpackSync extends Unpack {
796844 }
797845 }
798846
799- [ LINK ] ( entry , linkpath , link , _ ) {
847+ [ LINK ] ( entry , linkpath , link , done ) {
800848 try {
801849 fs [ link + 'Sync' ] ( linkpath , entry . absolute )
850+ done ( )
802851 entry . resume ( )
803852 } catch ( er ) {
804853 return this [ ONERROR ] ( er , entry )
0 commit comments