@@ -292,9 +292,27 @@ export class FileSystemService {
292292 throw new Error ( "Cannot move root folder" ) ;
293293 }
294294
295+ const resolvedDataDir = path . resolve ( this . dataDir ) ;
296+
297+ // Validate BOTH user-supplied paths up front — validatePath throws immediately on
298+ // any traversal attempt, producing safe absolute paths.
295299 const sourceFullPath = this . validatePath ( sourcePath ) ;
300+ const destParentFullPath = destinationParentPath
301+ ? this . validatePath ( destinationParentPath )
302+ : resolvedDataDir ;
303+
304+ // Explicit startsWith guards — recognised by static analysis as path-traversal
305+ // barriers and redundantly confirm both paths are inside the data directory.
306+ if ( ! sourceFullPath . startsWith ( resolvedDataDir + path . sep ) ) {
307+ throw new Error ( "Invalid path: source is outside data directory" ) ;
308+ }
309+ if ( destParentFullPath !== resolvedDataDir &&
310+ ! destParentFullPath . startsWith ( resolvedDataDir + path . sep ) ) {
311+ throw new Error ( "Invalid path: destination is outside data directory" ) ;
312+ }
296313
297- // Verify source exists and is a directory
314+ // Verify source exists and is a directory.
315+ // sourceFullPath is guarded by the startsWith check above.
298316 let sourceStat ;
299317 try {
300318 sourceStat = await fs . stat ( sourceFullPath ) ;
@@ -308,19 +326,32 @@ export class FileSystemService {
308326 throw new Error ( `${ sourcePath } is not a folder` ) ;
309327 }
310328
311- const folderName = path . basename ( sourcePath ) ;
312- const newRelativePath = destinationParentPath
313- ? ` ${ destinationParentPath } / ${ folderName } `
314- : folderName ;
329+ // Derive the folder name from the VALIDATED absolute path (not from the raw
330+ // user string). path.basename is a CodeQL-recognised sanitiser that strips
331+ // any remaining directory components.
332+ const folderName = path . basename ( sourceFullPath ) ;
315333
316- // Prevent moving a folder into itself or a descendant
317- if ( newRelativePath === sourcePath || newRelativePath . startsWith ( `${ sourcePath } /` ) ) {
318- throw new Error ( "Cannot move a folder into itself or one of its subfolders" ) ;
334+ // Build the destination absolute path entirely from validated components.
335+ // destParentFullPath was validated above; folderName came from path.basename.
336+ const newFullPath = path . join ( destParentFullPath , folderName ) ;
337+
338+ // Guard the computed destination path before any filesystem use.
339+ if ( ! newFullPath . startsWith ( resolvedDataDir + path . sep ) &&
340+ newFullPath !== resolvedDataDir ) {
341+ throw new Error ( "Invalid path: computed destination is outside data directory" ) ;
319342 }
320343
321- const newFullPath = this . validatePath ( newRelativePath ) ;
344+ // Relative path (for cache keys and return value) — derived from absolute paths.
345+ const newRelativePath = path . relative ( resolvedDataDir , newFullPath ) ;
322346
323- // Check destination doesn't already exist
347+ // Prevent moving a folder into itself or a descendant (compare absolute paths).
348+ if ( newFullPath === sourceFullPath ||
349+ newFullPath . startsWith ( sourceFullPath + path . sep ) ) {
350+ throw new Error ( "Cannot move a folder into itself or one of its subfolders" ) ;
351+ }
352+
353+ // Check destination doesn't already exist.
354+ // newFullPath is guarded by the startsWith check above.
324355 try {
325356 await fs . access ( newFullPath ) ;
326357 throw new Error ( `A folder already exists at: ${ newRelativePath } ` ) ;
@@ -331,12 +362,11 @@ export class FileSystemService {
331362 // ENOENT means destination doesn't exist — that's what we want
332363 }
333364
334- // Ensure the destination parent directory exists
335- if ( destinationParentPath ) {
336- const destParentFullPath = this . validatePath ( destinationParentPath ) ;
337- await fs . mkdir ( destParentFullPath , { recursive : true } ) ;
338- }
365+ // Ensure the destination parent directory exists.
366+ // destParentFullPath is guarded by the startsWith check above.
367+ await fs . mkdir ( destParentFullPath , { recursive : true } ) ;
339368
369+ // Both paths are guarded by startsWith checks above.
340370 await fs . rename ( sourceFullPath , newFullPath ) ;
341371
342372 // Invalidate all related caches
0 commit comments