Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit2f1bca0

Browse files
committed
fix: prune dirCache properly for unicode, windows
This prunes the dirCache in a way that catches unicode filename matches.If a symbolic link is encountered on Windows, the entire dirCache iscleared, as 8.3 shortname collisions may result in a path escapevulnerability in the case of symbolic links. If such a collision occursin the case of other types of entries, it is not such a big problem,because the unpack will fail.
1 parent9bf70a8 commit2f1bca0

File tree

2 files changed

+219
-22
lines changed

2 files changed

+219
-22
lines changed

‎lib/unpack.js‎

Lines changed: 76 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ const wc = require('./winchars.js')
1818
conststripAbsolutePath=require('./strip-absolute-path.js')
1919
constpathReservations=require('./path-reservations.js')
2020
constnormPath=require('./normalize-windows-path.js')
21+
conststripSlash=require('./strip-trailing-slashes.js')
2122

2223
constONENTRY=Symbol('onEntry')
2324
constCHECKFS=Symbol('checkFs')
2425
constCHECKFS2=Symbol('checkFs2')
26+
constPRUNECACHE=Symbol('pruneCache')
2527
constISREUSABLE=Symbol('isReusable')
2628
constMAKEFS=Symbol('makeFs')
2729
constFILE=Symbol('file')
@@ -45,6 +47,8 @@ const UID = Symbol('uid')
4547
constGID=Symbol('gid')
4648
constCHECKED_CWD=Symbol('checkedCwd')
4749
constcrypto=require('crypto')
50+
constplatform=process.env.TESTING_TAR_FAKE_PLATFORM||process.platform
51+
constisWindows=platform==='win32'
4852

4953
// Unlinks on Windows are not atomic.
5054
//
@@ -63,7 +67,7 @@ const crypto = require('crypto')
6367
// See: https://github.com/npm/node-tar/issues/183
6468
/* istanbul ignore next */
6569
constunlinkFile=(path,cb)=>{
66-
if(process.platform!=='win32')
70+
if(!isWindows)
6771
returnfs.unlink(path,cb)
6872

6973
constname=path+'.DELETE.'+crypto.randomBytes(16).toString('hex')
@@ -76,7 +80,7 @@ const unlinkFile = (path, cb) => {
7680

7781
/* istanbul ignore next */
7882
constunlinkFileSync=path=>{
79-
if(process.platform!=='win32')
83+
if(!isWindows)
8084
returnfs.unlinkSync(path)
8185

8286
constname=path+'.DELETE.'+crypto.randomBytes(16).toString('hex')
@@ -90,17 +94,33 @@ const uint32 = (a, b, c) =>
9094
:b===b>>>0 ?b
9195
:c
9296

97+
// clear the cache if it's a case-insensitive unicode-squashing match.
98+
// we can't know if the current file system is case-sensitive or supports
99+
// unicode fully, so we check for similarity on the maximally compatible
100+
// representation. Err on the side of pruning, since all it's doing is
101+
// preventing lstats, and it's not the end of the world if we get a false
102+
// positive.
103+
// Note that on windows, we always drop the entire cache whenever a
104+
// symbolic link is encountered, because 8.3 filenames are impossible
105+
// to reason about, and collisions are hazards rather than just failures.
106+
constcacheKeyNormalize=path=>stripSlash(normPath(path))
107+
.normalize('NFKD')
108+
.toLowerCase()
109+
93110
constpruneCache=(cache,abs)=>{
94-
// clear the cache if it's a case-insensitive match, since we can't
95-
// know if the current file system is case-sensitive or not.
96-
abs=normPath(abs).toLowerCase()
111+
abs=cacheKeyNormalize(abs)
97112
for(constpathofcache.keys()){
98-
constplower=path.toLowerCase()
99-
if(plower===abs||plower.toLowerCase().indexOf(abs+'/')===0)
113+
constpnorm=cacheKeyNormalize(path)
114+
if(pnorm===abs||pnorm.indexOf(abs+'/')===0)
100115
cache.delete(path)
101116
}
102117
}
103118

119+
constdropCache=cache=>{
120+
for(constkeyofcache.keys())
121+
cache.delete(key)
122+
}
123+
104124
classUnpackextendsParser{
105125
constructor(opt){
106126
if(!opt)
@@ -159,7 +179,7 @@ class Unpack extends Parser {
159179
this.forceChown=opt.forceChown===true
160180

161181
// turn ><?| in filenames into 0xf000-higher encoded forms
162-
this.win32=!!opt.win32||process.platform==='win32'
182+
this.win32=!!opt.win32||isWindows
163183

164184
// do not unpack over files that are newer than what's in the archive
165185
this.newer=!!opt.newer
@@ -470,7 +490,7 @@ class Unpack extends Parser {
470490
!this.unlink&&
471491
st.isFile()&&
472492
st.nlink<=1&&
473-
process.platform!=='win32'
493+
!isWindows
474494
}
475495

476496
// check if a thing is there, and if so, try to clobber it
@@ -481,13 +501,31 @@ class Unpack extends Parser {
481501
paths.push(entry.linkpath)
482502
this.reservations.reserve(paths,done=>this[CHECKFS2](entry,done))
483503
}
484-
[CHECKFS2](entry,done){
504+
505+
[PRUNECACHE](entry){
485506
// if we are not creating a directory, and the path is in the dirCache,
486507
// then that means we are about to delete the directory we created
487508
// previously, and it is no longer going to be a directory, and neither
488509
// is any of its children.
489-
if(entry.type!=='Directory')
510+
// If a symbolic link is encountered on Windows, all bets are off.
511+
// There is no reasonable way to sanitize the cache in such a way
512+
// we will be able to avoid having filesystem collisions. If this
513+
// happens with a non-symlink entry, it'll just fail to unpack,
514+
// but a symlink to a directory, using an 8.3 shortname, can evade
515+
// detection and lead to arbitrary writes to anywhere on the system.
516+
if(isWindows&&entry.type==='SymbolicLink')
517+
dropCache(this.dirCache)
518+
elseif(entry.type!=='Directory')
490519
pruneCache(this.dirCache,entry.absolute)
520+
}
521+
522+
[CHECKFS2](entry,fullyDone){
523+
this[PRUNECACHE](entry)
524+
525+
constdone=er=>{
526+
this[PRUNECACHE](entry)
527+
fullyDone(er)
528+
}
491529

492530
constcheckCwd=()=>{
493531
this[MKDIR](this.cwd,this.dmode,er=>{
@@ -538,7 +576,13 @@ class Unpack extends Parser {
538576
returnafterChmod()
539577
returnfs.chmod(entry.absolute,entry.mode,afterChmod)
540578
}
541-
// not a dir entry, have to remove it.
579+
// Not a dir entry, have to remove it.
580+
// NB: the only way to end up with an entry that is the cwd
581+
// itself, in such a way that == does not detect, is a
582+
// tricky windows absolute path with UNC or 8.3 parts (and
583+
// preservePaths:true, or else it will have been stripped).
584+
// In that case, the user has opted out of path protections
585+
// explicitly, so if they blow away the cwd, c'est la vie.
542586
if(entry.absolute!==this.cwd){
543587
returnfs.rmdir(entry.absolute,er=>
544588
this[MAKEFS](er,entry,done))
@@ -608,8 +652,7 @@ class UnpackSync extends Unpack {
608652
}
609653

610654
[CHECKFS](entry){
611-
if(entry.type!=='Directory')
612-
pruneCache(this.dirCache,entry.absolute)
655+
this[PRUNECACHE](entry)
613656

614657
if(!this[CHECKED_CWD]){
615658
conster=this[MKDIR](this.cwd,this.dmode)
@@ -658,13 +701,19 @@ class UnpackSync extends Unpack {
658701
this[MAKEFS](er,entry)
659702
}
660703

661-
[FILE](entry,_){
704+
[FILE](entry,done){
662705
constmode=entry.mode&0o7777||this.fmode
663706

664707
constoner=er=>{
665-
try{fs.closeSync(fd)}catch(_){}
666-
if(er)
667-
this[ONERROR](er,entry)
708+
letcloseError
709+
try{
710+
fs.closeSync(fd)
711+
}catch(e){
712+
closeError=e
713+
}
714+
if(er||closeError)
715+
this[ONERROR](er||closeError,entry)
716+
done()
668717
}
669718

670719
letstream
@@ -725,11 +774,14 @@ class UnpackSync extends Unpack {
725774
})
726775
}
727776

728-
[DIRECTORY](entry,_){
777+
[DIRECTORY](entry,done){
729778
constmode=entry.mode&0o7777||this.dmode
730779
conster=this[MKDIR](entry.absolute,mode)
731-
if(er)
732-
returnthis[ONERROR](er,entry)
780+
if(er){
781+
this[ONERROR](er,entry)
782+
done()
783+
return
784+
}
733785
if(entry.mtime&&!this.noMtime){
734786
try{
735787
fs.utimesSync(entry.absolute,entry.atime||newDate(),entry.mtime)
@@ -740,6 +792,7 @@ class UnpackSync extends Unpack {
740792
fs.chownSync(entry.absolute,this[UID](entry),this[GID](entry))
741793
}catch(er){}
742794
}
795+
done()
743796
entry.resume()
744797
}
745798

@@ -762,9 +815,10 @@ class UnpackSync extends Unpack {
762815
}
763816
}
764817

765-
[LINK](entry,linkpath,link,_){
818+
[LINK](entry,linkpath,link,done){
766819
try{
767820
fs[link+'Sync'](linkpath,entry.absolute)
821+
done()
768822
entry.resume()
769823
}catch(er){
770824
returnthis[ONERROR](er,entry)

‎test/unpack.js‎

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2693,3 +2693,146 @@ t.test('using strip option when top level file exists', t => {
26932693
check(t,path)
26942694
})
26952695
})
2696+
2697+
t.test('dirCache pruning unicode normalized collisions',{
2698+
skip:isWindows&&'symlinks not fully supported',
2699+
},t=>{
2700+
constdata=makeTar([
2701+
{
2702+
type:'Directory',
2703+
path:'foo',
2704+
},
2705+
{
2706+
type:'File',
2707+
path:'foo/bar',
2708+
size:1,
2709+
},
2710+
'x',
2711+
{
2712+
type:'Directory',
2713+
// café
2714+
path:Buffer.from([0x63,0x61,0x66,0xc3,0xa9]).toString(),
2715+
},
2716+
{
2717+
type:'SymbolicLink',
2718+
// cafe with a `
2719+
path:Buffer.from([0x63,0x61,0x66,0x65,0xcc,0x81]).toString(),
2720+
linkpath:'foo',
2721+
},
2722+
{
2723+
type:'File',
2724+
path:Buffer.from([0x63,0x61,0x66,0xc3,0xa9]).toString()+'/bar',
2725+
size:1,
2726+
},
2727+
'y',
2728+
'',
2729+
'',
2730+
])
2731+
2732+
constcheck=(path,dirCache,t)=>{
2733+
path=path.replace(/\\/g,'/')
2734+
t.strictSame([...dirCache.entries()],[
2735+
[path,true],
2736+
[`${path}/foo`,true],
2737+
])
2738+
t.equal(fs.readFileSync(path+'/foo/bar','utf8'),'x')
2739+
t.end()
2740+
}
2741+
2742+
t.test('sync',t=>{
2743+
constpath=t.testdir()
2744+
constdirCache=newMap()
2745+
newUnpackSync({cwd:path, dirCache}).end(data)
2746+
check(path,dirCache,t)
2747+
})
2748+
t.test('async',t=>{
2749+
constpath=t.testdir()
2750+
constdirCache=newMap()
2751+
newUnpack({cwd:path, dirCache})
2752+
.on('close',()=>check(path,dirCache,t))
2753+
.end(data)
2754+
})
2755+
2756+
t.end()
2757+
})
2758+
2759+
t.test('dircache prune all on windows when symlink encountered',t=>{
2760+
if(process.platform!=='win32'){
2761+
process.env.TESTING_TAR_FAKE_PLATFORM='win32'
2762+
t.teardown(()=>{
2763+
deleteprocess.env.TESTING_TAR_FAKE_PLATFORM
2764+
})
2765+
}
2766+
constsymlinks=[]
2767+
constUnpack=requireInject('../lib/unpack.js',{
2768+
fs:{
2769+
...fs,
2770+
symlink:(target,dest,cb)=>{
2771+
symlinks.push(['async',target,dest])
2772+
process.nextTick(cb)
2773+
},
2774+
symlinkSync:(target,dest)=>symlinks.push(['sync',target,dest]),
2775+
},
2776+
})
2777+
constUnpackSync=Unpack.Sync
2778+
2779+
constdata=makeTar([
2780+
{
2781+
type:'Directory',
2782+
path:'foo',
2783+
},
2784+
{
2785+
type:'File',
2786+
path:'foo/bar',
2787+
size:1,
2788+
},
2789+
'x',
2790+
{
2791+
type:'Directory',
2792+
// café
2793+
path:Buffer.from([0x63,0x61,0x66,0xc3,0xa9]).toString(),
2794+
},
2795+
{
2796+
type:'SymbolicLink',
2797+
// cafe with a `
2798+
path:Buffer.from([0x63,0x61,0x66,0x65,0xcc,0x81]).toString(),
2799+
linkpath:'safe/actually/but/cannot/be/too/careful',
2800+
},
2801+
{
2802+
type:'File',
2803+
path:'bar/baz',
2804+
size:1,
2805+
},
2806+
'z',
2807+
'',
2808+
'',
2809+
])
2810+
2811+
constcheck=(path,dirCache,t)=>{
2812+
// symlink blew away all dirCache entries before it
2813+
path=path.replace(/\\/g,'/')
2814+
t.strictSame([...dirCache.entries()],[
2815+
[`${path}/bar`,true],
2816+
])
2817+
t.equal(fs.readFileSync(`${path}/foo/bar`,'utf8'),'x')
2818+
t.equal(fs.readFileSync(`${path}/bar/baz`,'utf8'),'z')
2819+
t.end()
2820+
}
2821+
2822+
t.test('sync',t=>{
2823+
constpath=t.testdir()
2824+
constdirCache=newMap()
2825+
newUnpackSync({cwd:path, dirCache}).end(data)
2826+
check(path,dirCache,t)
2827+
})
2828+
2829+
t.test('async',t=>{
2830+
constpath=t.testdir()
2831+
constdirCache=newMap()
2832+
newUnpack({cwd:path, dirCache})
2833+
.on('close',()=>check(path,dirCache,t))
2834+
.end(data)
2835+
})
2836+
2837+
t.end()
2838+
})

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp