@@ -16,10 +16,12 @@ const wc = require('./winchars.js')
16
16
const pathReservations = require ( './path-reservations.js' )
17
17
const stripAbsolutePath = require ( './strip-absolute-path.js' )
18
18
const normPath = require ( './normalize-windows-path.js' )
19
+ const stripSlash = require ( './strip-trailing-slashes.js' )
19
20
20
21
const ONENTRY = Symbol ( 'onEntry' )
21
22
const CHECKFS = Symbol ( 'checkFs' )
22
23
const CHECKFS2 = Symbol ( 'checkFs2' )
24
+ const PRUNECACHE = Symbol ( 'pruneCache' )
23
25
const ISREUSABLE = Symbol ( 'isReusable' )
24
26
const MAKEFS = Symbol ( 'makeFs' )
25
27
const FILE = Symbol ( 'file' )
@@ -43,6 +45,8 @@ const GID = Symbol('gid')
43
45
const CHECKED_CWD = Symbol ( 'checkedCwd' )
44
46
const crypto = require ( 'crypto' )
45
47
const getFlag = require ( './get-write-flag.js' )
48
+ const platform = process . env . TESTING_TAR_FAKE_PLATFORM || process . platform
49
+ const isWindows = platform === 'win32'
46
50
47
51
// Unlinks on Windows are not atomic.
48
52
//
@@ -61,7 +65,7 @@ const getFlag = require('./get-write-flag.js')
61
65
// See: https://github.com/npm/node-tar/issues/183
62
66
/* istanbul ignore next */
63
67
const unlinkFile = ( path , cb ) => {
64
- if ( process . platform !== 'win32' )
68
+ if ( ! isWindows )
65
69
return fs . unlink ( path , cb )
66
70
67
71
const name = path + '.DELETE.' + crypto . randomBytes ( 16 ) . toString ( 'hex' )
@@ -74,7 +78,7 @@ const unlinkFile = (path, cb) => {
74
78
75
79
/* istanbul ignore next */
76
80
const unlinkFileSync = path => {
77
- if ( process . platform !== 'win32' )
81
+ if ( ! isWindows )
78
82
return fs . unlinkSync ( path )
79
83
80
84
const name = path + '.DELETE.' + crypto . randomBytes ( 16 ) . toString ( 'hex' )
@@ -88,17 +92,33 @@ const uint32 = (a, b, c) =>
88
92
: b === b >>> 0 ? b
89
93
: c
90
94
95
+ // clear the cache if it's a case-insensitive unicode-squashing match.
96
+ // we can't know if the current file system is case-sensitive or supports
97
+ // unicode fully, so we check for similarity on the maximally compatible
98
+ // representation. Err on the side of pruning, since all it's doing is
99
+ // preventing lstats, and it's not the end of the world if we get a false
100
+ // positive.
101
+ // Note that on windows, we always drop the entire cache whenever a
102
+ // symbolic link is encountered, because 8.3 filenames are impossible
103
+ // to reason about, and collisions are hazards rather than just failures.
104
+ const cacheKeyNormalize = path => stripSlash ( normPath ( path ) )
105
+ . normalize ( 'NFKD' )
106
+ . toLowerCase ( )
107
+
91
108
const pruneCache = ( cache , abs ) => {
92
- // clear the cache if it's a case-insensitive match, since we can't
93
- // know if the current file system is case-sensitive or not.
94
- abs = normPath ( abs ) . toLowerCase ( )
109
+ abs = cacheKeyNormalize ( abs )
95
110
for ( const path of cache . keys ( ) ) {
96
- const plower = path . toLowerCase ( )
97
- if ( plower === abs || plower . toLowerCase ( ) . indexOf ( abs + '/' ) === 0 )
111
+ const pnorm = cacheKeyNormalize ( path )
112
+ if ( pnorm === abs || pnorm . indexOf ( abs + '/' ) === 0 )
98
113
cache . delete ( path )
99
114
}
100
115
}
101
116
117
+ const dropCache = cache => {
118
+ for ( const key of cache . keys ( ) )
119
+ cache . delete ( key )
120
+ }
121
+
102
122
class Unpack extends Parser {
103
123
constructor ( opt ) {
104
124
if ( ! opt )
@@ -158,7 +178,7 @@ class Unpack extends Parser {
158
178
this . forceChown = opt . forceChown === true
159
179
160
180
// turn ><?| in filenames into 0xf000-higher encoded forms
161
- this . win32 = ! ! opt . win32 || process . platform === 'win32'
181
+ this . win32 = ! ! opt . win32 || isWindows
162
182
163
183
// do not unpack over files that are newer than what's in the archive
164
184
this . newer = ! ! opt . newer
@@ -497,7 +517,7 @@ class Unpack extends Parser {
497
517
! this . unlink &&
498
518
st . isFile ( ) &&
499
519
st . nlink <= 1 &&
500
- process . platform !== 'win32'
520
+ ! isWindows
501
521
}
502
522
503
523
// check if a thing is there, and if so, try to clobber it
@@ -509,13 +529,30 @@ class Unpack extends Parser {
509
529
this . reservations . reserve ( paths , done => this [ CHECKFS2 ] ( entry , done ) )
510
530
}
511
531
512
- [ CHECKFS2 ] ( entry , done ) {
532
+ [ PRUNECACHE ] ( entry ) {
513
533
// if we are not creating a directory, and the path is in the dirCache,
514
534
// then that means we are about to delete the directory we created
515
535
// previously, and it is no longer going to be a directory, and neither
516
536
// is any of its children.
517
- if ( entry . type !== 'Directory' )
537
+ // If a symbolic link is encountered on Windows, all bets are off.
538
+ // There is no reasonable way to sanitize the cache in such a way
539
+ // we will be able to avoid having filesystem collisions. If this
540
+ // happens with a non-symlink entry, it'll just fail to unpack,
541
+ // but a symlink to a directory, using an 8.3 shortname, can evade
542
+ // detection and lead to arbitrary writes to anywhere on the system.
543
+ if ( isWindows && entry . type === 'SymbolicLink' )
544
+ dropCache ( this . dirCache )
545
+ else if ( entry . type !== 'Directory' )
518
546
pruneCache ( this . dirCache , entry . absolute )
547
+ }
548
+
549
+ [ CHECKFS2 ] ( entry , fullyDone ) {
550
+ this [ PRUNECACHE ] ( entry )
551
+
552
+ const done = er => {
553
+ this [ PRUNECACHE ] ( entry )
554
+ fullyDone ( er )
555
+ }
519
556
520
557
const checkCwd = ( ) => {
521
558
this [ MKDIR ] ( this . cwd , this . dmode , er => {
@@ -566,7 +603,13 @@ class Unpack extends Parser {
566
603
return afterChmod ( )
567
604
return fs . chmod ( entry . absolute , entry . mode , afterChmod )
568
605
}
569
- // not a dir entry, have to remove it.
606
+ // Not a dir entry, have to remove it.
607
+ // NB: the only way to end up with an entry that is the cwd
608
+ // itself, in such a way that == does not detect, is a
609
+ // tricky windows absolute path with UNC or 8.3 parts (and
610
+ // preservePaths:true, or else it will have been stripped).
611
+ // In that case, the user has opted out of path protections
612
+ // explicitly, so if they blow away the cwd, c'est la vie.
570
613
if ( entry . absolute !== this . cwd ) {
571
614
return fs . rmdir ( entry . absolute , er =>
572
615
this [ MAKEFS ] ( er , entry , done ) )
@@ -641,8 +684,7 @@ class UnpackSync extends Unpack {
641
684
}
642
685
643
686
[ CHECKFS ] ( entry ) {
644
- if ( entry . type !== 'Directory' )
645
- pruneCache ( this . dirCache , entry . absolute )
687
+ this [ PRUNECACHE ] ( entry )
646
688
647
689
if ( ! this [ CHECKED_CWD ] ) {
648
690
const er = this [ MKDIR ] ( this . cwd , this . dmode )
@@ -691,7 +733,7 @@ class UnpackSync extends Unpack {
691
733
this [ MAKEFS ] ( er , entry )
692
734
}
693
735
694
- [ FILE ] ( entry , _ ) {
736
+ [ FILE ] ( entry , done ) {
695
737
const mode = entry . mode & 0o7777 || this . fmode
696
738
697
739
const oner = er => {
@@ -703,6 +745,7 @@ class UnpackSync extends Unpack {
703
745
}
704
746
if ( er || closeError )
705
747
this [ ONERROR ] ( er || closeError , entry )
748
+ done ( )
706
749
}
707
750
708
751
let fd
@@ -762,11 +805,14 @@ class UnpackSync extends Unpack {
762
805
} )
763
806
}
764
807
765
- [ DIRECTORY ] ( entry , _ ) {
808
+ [ DIRECTORY ] ( entry , done ) {
766
809
const mode = entry . mode & 0o7777 || this . dmode
767
810
const er = this [ MKDIR ] ( entry . absolute , mode )
768
- if ( er )
769
- return this [ ONERROR ] ( er , entry )
811
+ if ( er ) {
812
+ this [ ONERROR ] ( er , entry )
813
+ done ( )
814
+ return
815
+ }
770
816
if ( entry . mtime && ! this . noMtime ) {
771
817
try {
772
818
fs . utimesSync ( entry . absolute , entry . atime || new Date ( ) , entry . mtime )
@@ -777,6 +823,7 @@ class UnpackSync extends Unpack {
777
823
fs . chownSync ( entry . absolute , this [ UID ] ( entry ) , this [ GID ] ( entry ) )
778
824
} catch ( er ) { }
779
825
}
826
+ done ( )
780
827
entry . resume ( )
781
828
}
782
829
@@ -799,9 +846,10 @@ class UnpackSync extends Unpack {
799
846
}
800
847
}
801
848
802
- [ LINK ] ( entry , linkpath , link , _ ) {
849
+ [ LINK ] ( entry , linkpath , link , done ) {
803
850
try {
804
851
fs [ link + 'Sync' ] ( linkpath , entry . absolute )
852
+ done ( )
805
853
entry . resume ( )
806
854
} catch ( er ) {
807
855
return this [ ONERROR ] ( er , entry )
0 commit comments