@@ -18,6 +18,11 @@ import {
1818 type ListenerMap ,
1919 type UncaughtError ,
2020} from './PageCollector.js' ;
21+ import { Locator } from './third_party/index.js' ;
22+ import {
23+ PredefinedNetworkConditions ,
24+ fileURLToPath ,
25+ } from './third_party/index.js' ;
2126import type {
2227 Browser ,
2328 BrowserContext ,
@@ -31,8 +36,7 @@ import type {
3136 Extension ,
3237} from './third_party/index.js' ;
3338import type { DevTools } from './third_party/index.js' ;
34- import { Locator } from './third_party/index.js' ;
35- import { PredefinedNetworkConditions } from './third_party/index.js' ;
39+ import type { Root } from './third_party/index.js' ;
3640import { listPages } from './tools/pages.js' ;
3741import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js' ;
3842import type { Context , SupportedExtensions } from './tools/ToolDefinition.js' ;
@@ -90,6 +94,7 @@ export class McpContext implements Context {
9094 #locatorClass: typeof Locator ;
9195 #options: McpContextOptions ;
9296 #heapSnapshotManager = new HeapSnapshotManager ( ) ;
97+ #roots: Root [ ] = [ ] ;
9398
9499 private constructor (
95100 browser : Browser ,
@@ -154,6 +159,34 @@ export class McpContext implements Context {
154159 return context ;
155160 }
156161
162+ roots ( ) : Root [ ] {
163+ return this . #roots;
164+ }
165+
166+ setRoots ( roots : Root [ ] ) : void {
167+ this . #roots = roots ;
168+ }
169+
170+ validatePath ( filePath : string ) : void {
171+ const roots = this . roots ( ) ;
172+ if ( roots . length === 0 ) {
173+ return ;
174+ }
175+ const absolutePath = path . resolve ( filePath ) ;
176+ for ( const root of roots ) {
177+ const rootPath = path . resolve ( fileURLToPath ( root . uri ) ) ;
178+ if (
179+ absolutePath === rootPath ||
180+ absolutePath . startsWith ( rootPath + path . sep )
181+ ) {
182+ return ;
183+ }
184+ }
185+ throw new Error (
186+ `Access denied: path ${ filePath } is not within any of the workspace roots.` ,
187+ ) ;
188+ }
189+
157190 resolveCdpRequestId ( page : McpPage , cdpRequestId : string ) : number | undefined {
158191 if ( ! cdpRequestId ) {
159192 this . logger ( 'no network request' ) ;
@@ -643,13 +676,21 @@ export class McpContext implements Context {
643676 data : Uint8Array < ArrayBufferLike > ,
644677 filename : string ,
645678 ) : Promise < { filepath : string } > {
679+ if (
680+ filename . includes ( '/' ) ||
681+ filename . includes ( '\\' ) ||
682+ filename . includes ( '..' )
683+ ) {
684+ throw new Error ( `Invalid filename: ${ filename } ` ) ;
685+ }
646686 return await saveTemporaryFile ( data , filename ) ;
647687 }
648688 async saveFile (
649689 data : Uint8Array < ArrayBufferLike > ,
650690 clientProvidedFilePath : string ,
651691 extension : SupportedExtensions ,
652692 ) : Promise < { filename : string } > {
693+ this . validatePath ( clientProvidedFilePath ) ;
653694 try {
654695 const filePath = ensureExtension (
655696 path . resolve ( clientProvidedFilePath ) ,
@@ -721,6 +762,7 @@ export class McpContext implements Context {
721762 }
722763
723764 async installExtension ( extensionPath : string ) : Promise < string > {
765+ this . validatePath ( extensionPath ) ;
724766 const id = await this . browser . installExtension ( extensionPath ) ;
725767 return id ;
726768 }
@@ -751,25 +793,29 @@ export class McpContext implements Context {
751793 async getHeapSnapshotAggregates (
752794 filePath : string ,
753795 ) : Promise < Record < string , AggregatedInfoWithUid > > {
796+ this . validatePath ( filePath ) ;
754797 return await this . #heapSnapshotManager. getAggregates ( filePath ) ;
755798 }
756799
757800 async getHeapSnapshotStats (
758801 filePath : string ,
759802 ) : Promise < DevTools . HeapSnapshotModel . HeapSnapshotModel . Statistics > {
803+ this . validatePath ( filePath ) ;
760804 return await this . #heapSnapshotManager. getStats ( filePath ) ;
761805 }
762806
763807 async getHeapSnapshotStaticData (
764808 filePath : string ,
765809 ) : Promise < DevTools . HeapSnapshotModel . HeapSnapshotModel . StaticData | null > {
810+ this . validatePath ( filePath ) ;
766811 return await this . #heapSnapshotManager. getStaticData ( filePath ) ;
767812 }
768813
769814 async getHeapSnapshotNodesByUid (
770815 filePath : string ,
771816 uid : number ,
772817 ) : Promise < DevTools . HeapSnapshotModel . HeapSnapshotModel . ItemsRange > {
818+ this . validatePath ( filePath ) ;
773819 return await this . #heapSnapshotManager. getNodesByUid ( filePath , uid ) ;
774820 }
775821}
0 commit comments