@@ -18,10 +18,12 @@ const wc = require('./winchars.js')
18
18
const pathReservations = require ( './path-reservations.js' )
19
19
const stripAbsolutePath = require ( './strip-absolute-path.js' )
20
20
const normPath = require ( './normalize-windows-path.js' )
21
+ const stripSlash = require ( './strip-trailing-slashes.js' )
21
22
22
23
const ONENTRY = Symbol ( 'onEntry' )
23
24
const CHECKFS = Symbol ( 'checkFs' )
24
25
const CHECKFS2 = Symbol ( 'checkFs2' )
26
+ const PRUNECACHE = Symbol ( 'pruneCache' )
25
27
const ISREUSABLE = Symbol ( 'isReusable' )
26
28
const MAKEFS = Symbol ( 'makeFs' )
27
29
const FILE = Symbol ( 'file' )
@@ -46,6 +48,8 @@ const GID = Symbol('gid')
46
48
const CHECKED_CWD = Symbol ( 'checkedCwd' )
47
49
const crypto = require ( 'crypto' )
48
50
const getFlag = require ( './get-write-flag.js' )
51
+ const platform = process . env . TESTING_TAR_FAKE_PLATFORM || process . platform
52
+ const isWindows = platform === 'win32'
49
53
50
54
// Unlinks on Windows are not atomic.
51
55
//
@@ -64,7 +68,7 @@ const getFlag = require('./get-write-flag.js')
64
68
// See: https://github.com/npm/node-tar/issues/183
65
69
/* istanbul ignore next */
66
70
const unlinkFile = ( path , cb ) => {
67
- if ( process . platform !== 'win32' )
71
+ if ( ! isWindows )
68
72
return fs . unlink ( path , cb )
69
73
70
74
const name = path + '.DELETE.' + crypto . randomBytes ( 16 ) . toString ( 'hex' )
@@ -77,7 +81,7 @@ const unlinkFile = (path, cb) => {
77
81
78
82
/* istanbul ignore next */
79
83
const unlinkFileSync = path => {
80
- if ( process . platform !== 'win32' )
84
+ if ( ! isWindows )
81
85
return fs . unlinkSync ( path )
82
86
83
87
const name = path + '.DELETE.' + crypto . randomBytes ( 16 ) . toString ( 'hex' )
@@ -91,17 +95,33 @@ const uint32 = (a, b, c) =>
91
95
: b === b >>> 0 ? b
92
96
: c
93
97
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
+
94
111
const 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 )
98
113
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 )
101
116
cache . delete ( path )
102
117
}
103
118
}
104
119
120
+ const dropCache = cache => {
121
+ for ( const key of cache . keys ( ) )
122
+ cache . delete ( key )
123
+ }
124
+
105
125
class Unpack extends Parser {
106
126
constructor ( opt ) {
107
127
if ( ! opt )
@@ -160,7 +180,7 @@ class Unpack extends Parser {
160
180
this . forceChown = opt . forceChown === true
161
181
162
182
// turn ><?| in filenames into 0xf000-higher encoded forms
163
- this . win32 = ! ! opt . win32 || process . platform === 'win32'
183
+ this . win32 = ! ! opt . win32 || isWindows
164
184
165
185
// do not unpack over files that are newer than what's in the archive
166
186
this . newer = ! ! opt . newer
@@ -494,7 +514,7 @@ class Unpack extends Parser {
494
514
! this . unlink &&
495
515
st . isFile ( ) &&
496
516
st . nlink <= 1 &&
497
- process . platform !== 'win32'
517
+ ! isWindows
498
518
}
499
519
500
520
// check if a thing is there, and if so, try to clobber it
@@ -505,13 +525,31 @@ class Unpack extends Parser {
505
525
paths . push ( entry . linkpath )
506
526
this . reservations . reserve ( paths , done => this [ CHECKFS2 ] ( entry , done ) )
507
527
}
508
- [ CHECKFS2 ] ( entry , done ) {
528
+
529
+ [ PRUNECACHE ] ( entry ) {
509
530
// if we are not creating a directory, and the path is in the dirCache,
510
531
// then that means we are about to delete the directory we created
511
532
// previously, and it is no longer going to be a directory, and neither
512
533
// 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' )
514
543
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
+ }
515
553
516
554
const checkCwd = ( ) => {
517
555
this [ MKDIR ] ( this . cwd , this . dmode , er => {
@@ -562,7 +600,13 @@ class Unpack extends Parser {
562
600
return afterChmod ( )
563
601
return fs . chmod ( entry . absolute , entry . mode , afterChmod )
564
602
}
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.
566
610
if ( entry . absolute !== this . cwd ) {
567
611
return fs . rmdir ( entry . absolute , er =>
568
612
this [ MAKEFS ] ( er , entry , done ) )
@@ -637,8 +681,7 @@ class UnpackSync extends Unpack {
637
681
}
638
682
639
683
[ CHECKFS ] ( entry ) {
640
- if ( entry . type !== 'Directory' )
641
- pruneCache ( this . dirCache , entry . absolute )
684
+ this [ PRUNECACHE ] ( entry )
642
685
643
686
if ( ! this [ CHECKED_CWD ] ) {
644
687
const er = this [ MKDIR ] ( this . cwd , this . dmode )
@@ -687,7 +730,7 @@ class UnpackSync extends Unpack {
687
730
this [ MAKEFS ] ( er , entry )
688
731
}
689
732
690
- [ FILE ] ( entry , _ ) {
733
+ [ FILE ] ( entry , done ) {
691
734
const mode = entry . mode & 0o7777 || this . fmode
692
735
693
736
const oner = er => {
@@ -699,6 +742,7 @@ class UnpackSync extends Unpack {
699
742
}
700
743
if ( er || closeError )
701
744
this [ ONERROR ] ( er || closeError , entry )
745
+ done ( )
702
746
}
703
747
704
748
let stream
@@ -759,11 +803,14 @@ class UnpackSync extends Unpack {
759
803
} )
760
804
}
761
805
762
- [ DIRECTORY ] ( entry , _ ) {
806
+ [ DIRECTORY ] ( entry , done ) {
763
807
const mode = entry . mode & 0o7777 || this . dmode
764
808
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
+ }
767
814
if ( entry . mtime && ! this . noMtime ) {
768
815
try {
769
816
fs . utimesSync ( entry . absolute , entry . atime || new Date ( ) , entry . mtime )
@@ -774,6 +821,7 @@ class UnpackSync extends Unpack {
774
821
fs . chownSync ( entry . absolute , this [ UID ] ( entry ) , this [ GID ] ( entry ) )
775
822
} catch ( er ) { }
776
823
}
824
+ done ( )
777
825
entry . resume ( )
778
826
}
779
827
@@ -796,9 +844,10 @@ class UnpackSync extends Unpack {
796
844
}
797
845
}
798
846
799
- [ LINK ] ( entry , linkpath , link , _ ) {
847
+ [ LINK ] ( entry , linkpath , link , done ) {
800
848
try {
801
849
fs [ link + 'Sync' ] ( linkpath , entry . absolute )
850
+ done ( )
802
851
entry . resume ( )
803
852
} catch ( er ) {
804
853
return this [ ONERROR ] ( er , entry )
0 commit comments