55 */
66
77import fs from 'node:fs/promises' ;
8+ import fsPromises from 'node:fs/promises' ;
89import os from 'node:os' ;
910import path from 'node:path' ;
1011import { fileURLToPath , pathToFileURL } from 'node:url' ;
@@ -45,7 +46,11 @@ import type {
4546 GeolocationOptions ,
4647 ExtensionServiceWorker ,
4748} from './types.js' ;
48- import { ensureExtension , getTempFilePath } from './utils/files.js' ;
49+ import {
50+ ensureExtension ,
51+ getTempFilePath ,
52+ resolveCanonicalPath ,
53+ } from './utils/files.js' ;
4954import { getNetworkMultiplierFromString } from './WaitForHelper.js' ;
5055
5156interface McpContextOptions {
@@ -175,27 +180,58 @@ export class McpContext implements Context {
175180 this . #roots = roots ;
176181 }
177182
178- validatePath ( filePath ?: string ) : void {
183+ async validatePath ( filePath ?: string ) : Promise < void > {
179184 if ( filePath === undefined ) {
180185 return ;
181186 }
182187 const roots = this . roots ( ) ;
183188 if ( roots === undefined ) {
184189 return ;
185190 }
186- const absolutePath = path . resolve ( filePath ) ;
191+
192+ let canonicalPath : string ;
193+
194+ try {
195+ canonicalPath = await resolveCanonicalPath ( filePath ) ;
196+ } catch ( err ) {
197+ const errMsg = err instanceof Error ? err . message : String ( err ) ;
198+ console . error (
199+ `[MCP Context] Error resolving real path for ${ filePath } : ${ errMsg } ` ,
200+ ) ;
201+ throw new Error (
202+ `Access denied: Cannot resolve base path for ${ filePath } .` ,
203+ ) ;
204+ }
205+
206+ let allowed = false ;
187207 for ( const root of roots ) {
188- const rootPath = path . resolve ( fileURLToPath ( root . uri ) ) ;
189- if (
190- absolutePath === rootPath ||
191- absolutePath . startsWith ( rootPath + path . sep )
192- ) {
193- return ;
208+ try {
209+ const rootPathUri = root . uri ;
210+ const rootPath = path . resolve ( fileURLToPath ( rootPathUri ) ) ;
211+ const canonicalRoot = await fsPromises . realpath ( rootPath ) ;
212+
213+ if (
214+ canonicalPath === canonicalRoot ||
215+ canonicalPath . startsWith ( canonicalRoot + path . sep )
216+ ) {
217+ allowed = true ;
218+ break ;
219+ }
220+ } catch ( rootErr ) {
221+ const errMsg =
222+ rootErr instanceof Error ? rootErr . message : String ( rootErr ) ;
223+ console . warn (
224+ `[MCP Context] Could not resolve configured root ${ root . uri } : ${ errMsg } ` ,
225+ ) ;
226+ // Skip this root if it cannot be resolved.
194227 }
195228 }
196- throw new Error (
197- `Access denied: path ${ filePath } is not within any of the workspace roots ${ JSON . stringify ( roots ) } .` ,
198- ) ;
229+
230+ if ( ! allowed ) {
231+ throw new Error (
232+ `Access denied: path ${ filePath } (canonical: ${ canonicalPath } ) is not within any of the configured workspace roots.` ,
233+ ) ;
234+ }
199235 }
200236
201237 resolveCdpRequestId ( page : McpPage , cdpRequestId : string ) : number | undefined {
@@ -708,7 +744,7 @@ export class McpContext implements Context {
708744 filename : string ,
709745 ) : Promise < { filepath : string } > {
710746 const filepath = await getTempFilePath ( filename ) ;
711- this . validatePath ( filepath ) ;
747+ await this . validatePath ( filepath ) ;
712748 try {
713749 await fs . writeFile ( filepath , data ) ;
714750 } catch ( err ) {
@@ -722,7 +758,7 @@ export class McpContext implements Context {
722758 clientProvidedFilePath : string ,
723759 extension : SupportedExtensions ,
724760 ) : Promise < { filename : string } > {
725- this . validatePath ( clientProvidedFilePath ) ;
761+ await this . validatePath ( clientProvidedFilePath ) ;
726762 try {
727763 const filePath = ensureExtension (
728764 path . resolve ( clientProvidedFilePath ) ,
@@ -794,7 +830,7 @@ export class McpContext implements Context {
794830 }
795831
796832 async installExtension ( extensionPath : string ) : Promise < string > {
797- this . validatePath ( extensionPath ) ;
833+ await this . validatePath ( extensionPath ) ;
798834 const id = await this . browser . installExtension ( extensionPath ) ;
799835 return id ;
800836 }
@@ -825,36 +861,37 @@ export class McpContext implements Context {
825861 async getHeapSnapshotAggregates (
826862 filePath : string ,
827863 ) : Promise < Record < string , AggregatedInfoWithId > > {
828- this . validatePath ( filePath ) ;
864+ await this . validatePath ( filePath ) ;
829865 return await this . #heapSnapshotManager. getAggregates ( filePath ) ;
830866 }
831867
832868 async getHeapSnapshotStats (
833869 filePath : string ,
834870 ) : Promise < DevTools . HeapSnapshotModel . HeapSnapshotModel . Statistics > {
835- this . validatePath ( filePath ) ;
871+ await this . validatePath ( filePath ) ;
836872 return await this . #heapSnapshotManager. getStats ( filePath ) ;
837873 }
838874
839875 async getHeapSnapshotStaticData (
840876 filePath : string ,
841877 ) : Promise < DevTools . HeapSnapshotModel . HeapSnapshotModel . StaticData | null > {
842- this . validatePath ( filePath ) ;
878+ await this . validatePath ( filePath ) ;
843879 return await this . #heapSnapshotManager. getStaticData ( filePath ) ;
844880 }
845881
846882 async getHeapSnapshotNodesById (
847883 filePath : string ,
848884 id : number ,
849885 ) : Promise < DevTools . HeapSnapshotModel . HeapSnapshotModel . ItemsRange > {
850- this . validatePath ( filePath ) ;
886+ await this . validatePath ( filePath ) ;
851887 return await this . #heapSnapshotManager. getNodesById ( filePath , id ) ;
852888 }
853889
854890 async getHeapSnapshotRetainers (
855891 filePath : string ,
856892 nodeId : number ,
857893 ) : Promise < DevTools . HeapSnapshotModel . HeapSnapshotModel . ItemsRange > {
894+ await this . validatePath ( filePath ) ;
858895 return await this . #heapSnapshotManager. getRetainers ( filePath , nodeId ) ;
859896 }
860897}
0 commit comments