@@ -143,6 +143,71 @@ test("doesn't download anything when two install/uninstall tasks are queued", as
143143 t . is ( downloadFileCallCount , 1 )
144144} )
145145
146+ test ( 'does not uninstall paths outside extensions from poisoned mapping' , async ( t ) => {
147+ await packageManager . syncComponents ( [ fakeComponent ( ) ] )
148+ await new Promise ( ( resolve ) => setTimeout ( resolve , 200 ) )
149+
150+ const escapeDir = path . join ( tmpDir . path , 'escape' )
151+ const markerPath = path . join ( escapeDir , 'marker.txt' )
152+ await ensureDirectoryExists ( escapeDir )
153+ await fs . writeFile ( markerPath , 'keep' )
154+
155+ const poisonedLocations = [ path . join ( 'Extensions' , '..' , 'escape' ) , path . join ( '..' , 'escape' ) ]
156+
157+ for ( const location of poisonedLocations ) {
158+ await fs . writeFile (
159+ path . join ( contentDir , 'mapping.json' ) ,
160+ JSON . stringify ( {
161+ [ uuid ] : { location, version } ,
162+ } ) ,
163+ )
164+
165+ await packageManager . syncComponents ( [ fakeComponent ( { deleted : true } ) ] )
166+ await new Promise ( ( resolve ) => setTimeout ( resolve , 200 ) )
167+
168+ t . true ( await fs . stat ( markerPath ) . then ( ( ) => true ) , `marker preserved for location ${ location } ` )
169+ t . deepEqual ( await readJSONFile ( path . join ( contentDir , 'mapping.json' ) ) , {
170+ [ uuid ] : { location, version } ,
171+ } )
172+ }
173+
174+ t . true ( await fs . stat ( path . join ( contentDir , identifier ) ) . then ( ( ) => true ) )
175+ } )
176+
177+ test ( 'rejects path traversal in package identifier' , async ( t ) => {
178+ const traversalIdentifiers = [ '../escape' , 'foo/../../escape' , '..' , '.' , 'foo/bar' ]
179+ const extensionsParent = path . dirname ( contentDir )
180+
181+ for ( const badIdentifier of traversalIdentifiers ) {
182+ const before = await fs . readdir ( contentDir )
183+ const parentBefore = await fs . readdir ( extensionsParent )
184+
185+ downloadFileCallCount = 0
186+ await packageManager . syncComponents ( [
187+ {
188+ ...fakeComponent ( { modifier : badIdentifier } ) ,
189+ content : {
190+ ...fakeComponent ( ) . content ,
191+ name : `Bad ${ badIdentifier } ` ,
192+ package_info : {
193+ ...fakeComponent ( ) . content . package_info ,
194+ identifier : badIdentifier ,
195+ } ,
196+ } ,
197+ } ,
198+ ] )
199+ await new Promise ( ( resolve ) => setTimeout ( resolve , 200 ) )
200+
201+ t . is ( downloadFileCallCount , 0 , `should not download for identifier ${ badIdentifier } ` )
202+ t . deepEqual ( await fs . readdir ( contentDir ) , before , `extensions dir unchanged for ${ badIdentifier } ` )
203+ t . deepEqual (
204+ await fs . readdir ( extensionsParent ) ,
205+ parentBefore ,
206+ `no directory escape for ${ badIdentifier } ` ,
207+ )
208+ }
209+ } )
210+
146211test ( "Relies on download_url's version field to store the version number" , async ( t ) => {
147212 await packageManager . syncComponents ( [ fakeComponent ( ) ] )
148213 await new Promise ( ( resolve ) => setTimeout ( resolve , 200 ) )
0 commit comments