@@ -202,6 +202,9 @@ export class FileBackend implements StorageBackendAdapter {
202202 httpStatusCode : 200 ,
203203 }
204204 } catch ( err : any ) {
205+ if ( err instanceof StorageBackendError ) {
206+ throw err
207+ }
205208 throw StorageBackendError . fromError ( err )
206209 }
207210 }
@@ -341,15 +344,18 @@ export class FileBackend implements StorageBackendAdapter {
341344 cacheControl : string
342345 ) : Promise < string | undefined > {
343346 const uploadId = randomUUID ( )
344- const multiPartFolder = path . join (
345- this . filePath ,
346- 'multiparts' ,
347- uploadId ,
348- bucketName ,
349- withOptionalVersion ( key , version )
347+ const multiPartFolder = this . resolveSecurePath (
348+ path . join ( 'multiparts' , uploadId , bucketName , withOptionalVersion ( key , version ) )
349+ )
350+ const multipartFile = this . resolveSecurePath (
351+ path . join (
352+ 'multiparts' ,
353+ uploadId ,
354+ bucketName ,
355+ withOptionalVersion ( key , version ) ,
356+ 'metadata.json'
357+ )
350358 )
351-
352- const multipartFile = path . join ( multiPartFolder , 'metadata.json' )
353359 await fsExtra . ensureDir ( multiPartFolder )
354360 await fsExtra . writeFile ( multipartFile , JSON . stringify ( { contentType, cacheControl } ) )
355361
@@ -364,16 +370,16 @@ export class FileBackend implements StorageBackendAdapter {
364370 partNumber : number ,
365371 body : stream . Readable
366372 ) : Promise < { ETag ?: string } > {
367- const multiPartFolder = path . join (
368- this . filePath ,
369- 'multiparts' ,
370- uploadId ,
371- bucketName ,
372- withOptionalVersion ( key , version )
373+ const partPath = this . resolveSecurePath (
374+ path . join (
375+ 'multiparts' ,
376+ uploadId ,
377+ bucketName ,
378+ withOptionalVersion ( key , version ) ,
379+ `part-${ partNumber } `
380+ )
373381 )
374382
375- const partPath = path . join ( multiPartFolder , `part-${ partNumber } ` )
376-
377383 const writeStream = fsExtra . createWriteStream ( partPath )
378384
379385 await pipeline ( body , writeStream )
@@ -399,16 +405,16 @@ export class FileBackend implements StorageBackendAdapter {
399405 version : string
400406 }
401407 > {
402- const multiPartFolder = path . join (
403- this . filePath ,
404- 'multiparts' ,
405- uploadId ,
406- bucketName ,
407- withOptionalVersion ( key , version )
408- )
409-
410408 const partsByEtags = parts . map ( async ( part ) => {
411- const partFilePath = path . join ( multiPartFolder , `part-${ part . PartNumber } ` )
409+ const partFilePath = this . resolveSecurePath (
410+ path . join (
411+ 'multiparts' ,
412+ uploadId ,
413+ bucketName ,
414+ withOptionalVersion ( key , version ) ,
415+ `part-${ part . PartNumber } `
416+ )
417+ )
412418 const partExists = await fsExtra . pathExists ( partFilePath )
413419
414420 if ( partExists ) {
@@ -432,7 +438,15 @@ export class FileBackend implements StorageBackendAdapter {
432438
433439 const multistream = new MultiStream ( fileStreams )
434440 const metadataContent = await fsExtra . readFile (
435- path . join ( multiPartFolder , 'metadata.json' ) ,
441+ this . resolveSecurePath (
442+ path . join (
443+ 'multiparts' ,
444+ uploadId ,
445+ bucketName ,
446+ withOptionalVersion ( key , version ) ,
447+ 'metadata.json'
448+ )
449+ ) ,
436450 'utf-8'
437451 )
438452
@@ -447,7 +461,7 @@ export class FileBackend implements StorageBackendAdapter {
447461 metadata . cacheControl
448462 )
449463
450- fsExtra . remove ( path . join ( this . filePath , 'multiparts' , uploadId ) ) . catch ( ( ) => {
464+ fsExtra . remove ( this . resolveSecurePath ( path . join ( 'multiparts' , uploadId ) ) ) . catch ( ( ) => {
451465 // no-op
452466 } )
453467
@@ -465,7 +479,7 @@ export class FileBackend implements StorageBackendAdapter {
465479 uploadId : string ,
466480 version ?: string
467481 ) : Promise < void > {
468- const multiPartFolder = path . join ( this . filePath , 'multiparts' , uploadId )
482+ const multiPartFolder = this . resolveSecurePath ( path . join ( 'multiparts' , uploadId ) )
469483
470484 await fsExtra . remove ( multiPartFolder )
471485
@@ -487,15 +501,15 @@ export class FileBackend implements StorageBackendAdapter {
487501 sourceVersion ?: string ,
488502 rangeBytes ?: { fromByte : number ; toByte : number }
489503 ) : Promise < { eTag ?: string ; lastModified ?: Date } > {
490- const multiPartFolder = path . join (
491- this . filePath ,
492- 'multiparts' ,
493- UploadId ,
494- storageS3Bucket ,
495- withOptionalVersion ( key , version )
504+ const partFilePath = this . resolveSecurePath (
505+ path . join (
506+ 'multiparts' ,
507+ UploadId ,
508+ storageS3Bucket ,
509+ withOptionalVersion ( key , version ) ,
510+ `part-${ PartNumber } `
511+ )
496512 )
497-
498- const partFilePath = path . join ( multiPartFolder , `part-${ PartNumber } ` )
499513 const sourceFilePath = this . resolveSecurePath (
500514 `${ storageS3Bucket } /${ withOptionalVersion ( sourceKey , sourceVersion ) } `
501515 )
@@ -624,6 +638,29 @@ export class FileBackend implements StorageBackendAdapter {
624638 * @throws {StorageBackendError } If the resolved path escapes the storage directory
625639 */
626640 private resolveSecurePath ( relativePath : string ) : string {
641+ if ( relativePath . includes ( '\0' ) ) {
642+ throw ERRORS . InvalidKey ( `Invalid key: ${ relativePath } contains null byte` )
643+ }
644+
645+ if ( path . isAbsolute ( relativePath ) ) {
646+ throw ERRORS . InvalidKey ( `Invalid key: ${ relativePath } must be a relative path` )
647+ }
648+
649+ const isWindowsDriveAbsolutePath = / ^ [ a - z A - Z ] : [ \\ / ] / . test ( relativePath )
650+ const isWindowsUncPath = / ^ \\ \\ [ ^ \\ / ] + [ \\ / ] [ ^ \\ / ] + / . test ( relativePath )
651+ if ( isWindowsDriveAbsolutePath || isWindowsUncPath ) {
652+ throw ERRORS . InvalidKey ( `Invalid key: ${ relativePath } must not be an absolute Windows path` )
653+ }
654+
655+ const hasDotTraversalSegment = relativePath
656+ . split ( / [ \\ / ] + / )
657+ . filter ( Boolean )
658+ . some ( ( segment ) => segment === '.' || segment === '..' )
659+
660+ if ( hasDotTraversalSegment ) {
661+ throw ERRORS . InvalidKey ( `Path traversal detected: ${ relativePath } contains dot path segment` )
662+ }
663+
627664 const resolvedPath = path . resolve ( this . filePath , relativePath )
628665 const normalizedPath = path . normalize ( resolvedPath )
629666
0 commit comments