@@ -84,7 +84,7 @@ export class FileBackend implements StorageBackendAdapter {
8484 headers ?: BrowserCacheHeaders
8585 ) : Promise < ObjectResponse > {
8686 // 'Range: bytes=#######-######
87- const file = path . resolve ( this . filePath , withOptionalVersion ( `${ bucketName } /${ key } ` , version ) )
87+ const file = this . resolveSecurePath ( withOptionalVersion ( `${ bucketName } /${ key } ` , version ) )
8888 const data = await fs . stat ( file )
8989 const eTag = await this . etag ( file , data )
9090 const fileSize = data . size
@@ -185,7 +185,7 @@ export class FileBackend implements StorageBackendAdapter {
185185 cacheControl : string
186186 ) : Promise < ObjectMetadata > {
187187 try {
188- const file = path . resolve ( this . filePath , withOptionalVersion ( `${ bucketName } /${ key } ` , version ) )
188+ const file = this . resolveSecurePath ( withOptionalVersion ( `${ bucketName } /${ key } ` , version ) )
189189 await fs . ensureFile ( file )
190190 const destFile = fs . createWriteStream ( file )
191191 await pipeline ( body , destFile )
@@ -214,7 +214,7 @@ export class FileBackend implements StorageBackendAdapter {
214214 */
215215 async deleteObject ( bucket : string , key : string , version : string | undefined ) : Promise < void > {
216216 try {
217- const file = path . resolve ( this . filePath , withOptionalVersion ( `${ bucket } /${ key } ` , version ) )
217+ const file = this . resolveSecurePath ( withOptionalVersion ( `${ bucket } /${ key } ` , version ) )
218218 await fs . remove ( file )
219219
220220 // Clean up empty parent directories
@@ -246,9 +246,8 @@ export class FileBackend implements StorageBackendAdapter {
246246 destinationVersion : string ,
247247 metadata : { cacheControl ?: string ; contentType ?: string }
248248 ) : Promise < Pick < ObjectMetadata , 'httpStatusCode' | 'eTag' | 'lastModified' > > {
249- const srcFile = path . resolve ( this . filePath , withOptionalVersion ( `${ bucket } /${ source } ` , version ) )
250- const destFile = path . resolve (
251- this . filePath ,
249+ const srcFile = this . resolveSecurePath ( withOptionalVersion ( `${ bucket } /${ source } ` , version ) )
250+ const destFile = this . resolveSecurePath (
252251 withOptionalVersion ( `${ bucket } /${ destination } ` , destinationVersion )
253252 )
254253
@@ -275,7 +274,7 @@ export class FileBackend implements StorageBackendAdapter {
275274 */
276275 async deleteObjects ( bucket : string , prefixes : string [ ] ) : Promise < void > {
277276 const promises = prefixes . map ( ( prefix ) => {
278- return fs . rm ( path . resolve ( this . filePath , bucket , prefix ) )
277+ return fs . rm ( this . resolveSecurePath ( ` ${ bucket } / ${ prefix } ` ) )
279278 } )
280279 const results = await Promise . allSettled ( promises )
281280
@@ -290,7 +289,7 @@ export class FileBackend implements StorageBackendAdapter {
290289 throw result . reason
291290 } else {
292291 // Add parent directory of successfully deleted file
293- const filePath = path . resolve ( this . filePath , bucket , prefixes [ index ] )
292+ const filePath = this . resolveSecurePath ( ` ${ bucket } / ${ prefixes [ index ] } ` )
294293 parentDirs . add ( path . dirname ( filePath ) )
295294 }
296295 } )
@@ -299,7 +298,7 @@ export class FileBackend implements StorageBackendAdapter {
299298 for ( const dir of parentDirs ) {
300299 try {
301300 await this . cleanupEmptyDirectories ( dir )
302- } catch ( e ) {
301+ } catch {
303302 // Ignore cleanup errors to not affect the main deletion operation
304303 }
305304 }
@@ -316,7 +315,7 @@ export class FileBackend implements StorageBackendAdapter {
316315 key : string ,
317316 version : string | undefined
318317 ) : Promise < ObjectMetadata > {
319- const file = path . join ( this . filePath , withOptionalVersion ( `${ bucket } /${ key } ` , version ) )
318+ const file = this . resolveSecurePath ( withOptionalVersion ( `${ bucket } /${ key } ` , version ) )
320319
321320 const data = await fs . stat ( file )
322321 const { cacheControl, contentType } = await this . getFileMetadata ( file )
@@ -474,7 +473,7 @@ export class FileBackend implements StorageBackendAdapter {
474473 // Clean up empty parent directories
475474 try {
476475 await this . cleanupEmptyDirectories ( path . dirname ( multiPartFolder ) )
477- } catch ( e ) {
476+ } catch {
478477 // Ignore cleanup errors
479478 }
480479 }
@@ -498,10 +497,8 @@ export class FileBackend implements StorageBackendAdapter {
498497 )
499498
500499 const partFilePath = path . join ( multiPartFolder , `part-${ PartNumber } ` )
501- const sourceFilePath = path . join (
502- this . filePath ,
503- storageS3Bucket ,
504- withOptionalVersion ( sourceKey , sourceVersion )
500+ const sourceFilePath = this . resolveSecurePath (
501+ `${ storageS3Bucket } /${ withOptionalVersion ( sourceKey , sourceVersion ) } `
505502 )
506503
507504 const platform = process . platform == 'darwin' ? 'darwin' : 'linux'
@@ -532,7 +529,7 @@ export class FileBackend implements StorageBackendAdapter {
532529 * @param version
533530 */
534531 async privateAssetUrl ( bucket : string , key : string , version : string | undefined ) : Promise < string > {
535- return 'local:///' + path . join ( this . filePath , withOptionalVersion ( `${ bucket } /${ key } ` , version ) )
532+ return 'local:///' + this . resolveSecurePath ( withOptionalVersion ( `${ bucket } /${ key } ` , version ) )
536533 }
537534
538535 async setFileMetadata ( file : string , { contentType, cacheControl } : FileMetadata ) {
@@ -582,7 +579,7 @@ export class FileBackend implements StorageBackendAdapter {
582579 await directory . close ( )
583580
584581 return entry === null
585- } catch ( error ) {
582+ } catch {
586583 return false
587584 }
588585 }
@@ -614,14 +611,33 @@ export class FileBackend implements StorageBackendAdapter {
614611 const parentDir = path . dirname ( dirPath )
615612 await this . cleanupEmptyDirectories ( parentDir )
616613 }
617- } catch ( e : any ) {
614+ } catch {
618615 // Ignore errors during cleanup to not affect main operations
619616 // Could be permission issues, concurrent access, directory not empty due to race conditions, etc.
620617 // Optional: Log for debugging purposes (uncomment if needed)
621618 // console.debug('Directory cleanup failed:', dirPath, e.message)
622619 }
623620 }
624621
622+ /**
623+ * Securely resolves a path within the storage directory, preventing path traversal attacks
624+ * @param relativePath The relative path to resolve
625+ * @throws {StorageBackendError } If the resolved path escapes the storage directory
626+ */
627+ private resolveSecurePath ( relativePath : string ) : string {
628+ const resolvedPath = path . resolve ( this . filePath , relativePath )
629+ const normalizedPath = path . normalize ( resolvedPath )
630+
631+ // Ensure the resolved path is within the storage directory
632+ if ( ! normalizedPath . startsWith ( this . filePath + path . sep ) && normalizedPath !== this . filePath ) {
633+ throw ERRORS . InvalidKey (
634+ `Path traversal detected: ${ relativePath } resolves outside storage directory`
635+ )
636+ }
637+
638+ return normalizedPath
639+ }
640+
625641 private async etag ( file : string , stats : fs . Stats ) : Promise < string > {
626642 if ( this . etagAlgorithm === 'md5' ) {
627643 const checksum = await fileChecksum ( file )
0 commit comments