66
77import fs from 'node:fs/promises' ;
88import path from 'node:path' ;
9+ import { fileURLToPath } from 'node:url' ;
910
1011import type { TargetUniverse } from './DevtoolsUtils.js' ;
1112import { UniverseManager } from './DevtoolsUtils.js' ;
@@ -18,21 +19,22 @@ import {
1819 type ListenerMap ,
1920 type UncaughtError ,
2021} from './PageCollector.js' ;
21- import type {
22- Browser ,
23- BrowserContext ,
24- ConsoleMessage ,
25- Debugger ,
26- HTTPRequest ,
27- Page ,
28- ScreenRecorder ,
29- Viewport ,
30- Target ,
31- Extension ,
22+ import {
23+ Locator ,
24+ PredefinedNetworkConditions ,
25+ type Browser ,
26+ type BrowserContext ,
27+ type ConsoleMessage ,
28+ type Debugger ,
29+ type HTTPRequest ,
30+ type Page ,
31+ type ScreenRecorder ,
32+ type Viewport ,
33+ type Target ,
34+ type Extension ,
35+ type Root ,
36+ type DevTools ,
3237} from './third_party/index.js' ;
33- import type { DevTools } from './third_party/index.js' ;
34- import { Locator } from './third_party/index.js' ;
35- import { PredefinedNetworkConditions } from './third_party/index.js' ;
3638import { listPages } from './tools/pages.js' ;
3739import { CLOSE_PAGE_ERROR } from './tools/ToolDefinition.js' ;
3840import type { Context , SupportedExtensions } from './tools/ToolDefinition.js' ;
@@ -90,6 +92,7 @@ export class McpContext implements Context {
9092 #locatorClass: typeof Locator ;
9193 #options: McpContextOptions ;
9294 #heapSnapshotManager = new HeapSnapshotManager ( ) ;
95+ #roots: Root [ ] | undefined = undefined ;
9396
9497 private constructor (
9598 browser : Browser ,
@@ -154,6 +157,34 @@ export class McpContext implements Context {
154157 return context ;
155158 }
156159
160+ roots ( ) : Root [ ] | undefined {
161+ return this . #roots;
162+ }
163+
164+ setRoots ( roots : Root [ ] | undefined ) : void {
165+ this . #roots = roots ;
166+ }
167+
168+ validatePath ( filePath : string ) : void {
169+ const roots = this . roots ( ) ;
170+ if ( roots === undefined ) {
171+ return ;
172+ }
173+ const absolutePath = path . resolve ( filePath ) ;
174+ for ( const root of roots ) {
175+ const rootPath = path . resolve ( fileURLToPath ( root . uri ) ) ;
176+ if (
177+ absolutePath === rootPath ||
178+ absolutePath . startsWith ( rootPath + path . sep )
179+ ) {
180+ return ;
181+ }
182+ }
183+ throw new Error (
184+ `Access denied: path ${ filePath } is not within any of the workspace roots ${ JSON . stringify ( roots ) } .` ,
185+ ) ;
186+ }
187+
157188 resolveCdpRequestId ( page : McpPage , cdpRequestId : string ) : number | undefined {
158189 if ( ! cdpRequestId ) {
159190 this . logger ( 'no network request' ) ;
@@ -643,13 +674,21 @@ export class McpContext implements Context {
643674 data : Uint8Array < ArrayBufferLike > ,
644675 filename : string ,
645676 ) : Promise < { filepath : string } > {
677+ if (
678+ filename . includes ( '/' ) ||
679+ filename . includes ( '\\' ) ||
680+ filename . includes ( '..' )
681+ ) {
682+ throw new Error ( `Invalid filename: ${ filename } ` ) ;
683+ }
646684 return await saveTemporaryFile ( data , filename ) ;
647685 }
648686 async saveFile (
649687 data : Uint8Array < ArrayBufferLike > ,
650688 clientProvidedFilePath : string ,
651689 extension : SupportedExtensions ,
652690 ) : Promise < { filename : string } > {
691+ this . validatePath ( clientProvidedFilePath ) ;
653692 try {
654693 const filePath = ensureExtension (
655694 path . resolve ( clientProvidedFilePath ) ,
@@ -721,6 +760,7 @@ export class McpContext implements Context {
721760 }
722761
723762 async installExtension ( extensionPath : string ) : Promise < string > {
763+ this . validatePath ( extensionPath ) ;
724764 const id = await this . browser . installExtension ( extensionPath ) ;
725765 return id ;
726766 }
@@ -751,25 +791,29 @@ export class McpContext implements Context {
751791 async getHeapSnapshotAggregates (
752792 filePath : string ,
753793 ) : Promise < Record < string , AggregatedInfoWithUid > > {
794+ this . validatePath ( filePath ) ;
754795 return await this . #heapSnapshotManager. getAggregates ( filePath ) ;
755796 }
756797
757798 async getHeapSnapshotStats (
758799 filePath : string ,
759800 ) : Promise < DevTools . HeapSnapshotModel . HeapSnapshotModel . Statistics > {
801+ this . validatePath ( filePath ) ;
760802 return await this . #heapSnapshotManager. getStats ( filePath ) ;
761803 }
762804
763805 async getHeapSnapshotStaticData (
764806 filePath : string ,
765807 ) : Promise < DevTools . HeapSnapshotModel . HeapSnapshotModel . StaticData | null > {
808+ this . validatePath ( filePath ) ;
766809 return await this . #heapSnapshotManager. getStaticData ( filePath ) ;
767810 }
768811
769812 async getHeapSnapshotNodesByUid (
770813 filePath : string ,
771814 uid : number ,
772815 ) : Promise < DevTools . HeapSnapshotModel . HeapSnapshotModel . ItemsRange > {
816+ this . validatePath ( filePath ) ;
773817 return await this . #heapSnapshotManager. getNodesByUid ( filePath , uid ) ;
774818 }
775819}
0 commit comments