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' ;
@@ -42,7 +44,7 @@ import type {
4244 GeolocationOptions ,
4345 ExtensionServiceWorker ,
4446} from './types.js' ;
45- import { ensureExtension , saveTemporaryFile } from './utils/files.js' ;
47+ import { ensureExtension , getTempFilePath } from './utils/files.js' ;
4648import { getNetworkMultiplierFromString } from './WaitForHelper.js' ;
4749
4850interface McpContextOptions {
@@ -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,37 @@ 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+ if ( filePath === undefined ) {
170+ return ;
171+ }
172+ const roots = this . roots ( ) ;
173+ if ( roots === undefined ) {
174+ return ;
175+ }
176+ const absolutePath = path . resolve ( filePath ) ;
177+ for ( const root of roots ) {
178+ const rootPath = path . resolve ( fileURLToPath ( root . uri ) ) ;
179+ if (
180+ absolutePath === rootPath ||
181+ absolutePath . startsWith ( rootPath + path . sep )
182+ ) {
183+ return ;
184+ }
185+ }
186+ throw new Error (
187+ `Access denied: path ${ filePath } is not within any of the workspace roots ${ JSON . stringify ( roots ) } .` ,
188+ ) ;
189+ }
190+
157191 resolveCdpRequestId ( page : McpPage , cdpRequestId : string ) : number | undefined {
158192 if ( ! cdpRequestId ) {
159193 this . logger ( 'no network request' ) ;
@@ -643,13 +677,22 @@ export class McpContext implements Context {
643677 data : Uint8Array < ArrayBufferLike > ,
644678 filename : string ,
645679 ) : Promise < { filepath : string } > {
646- return await saveTemporaryFile ( data , filename ) ;
680+ const filepath = await getTempFilePath ( filename ) ;
681+ this . validatePath ( filepath ) ;
682+ try {
683+ await fs . writeFile ( filepath , data ) ;
684+ } catch ( err ) {
685+ throw new Error ( 'Could not save a file' , { cause : err } ) ;
686+ }
687+ return { filepath} ;
647688 }
689+
648690 async saveFile (
649691 data : Uint8Array < ArrayBufferLike > ,
650692 clientProvidedFilePath : string ,
651693 extension : SupportedExtensions ,
652694 ) : Promise < { filename : string } > {
695+ this . validatePath ( clientProvidedFilePath ) ;
653696 try {
654697 const filePath = ensureExtension (
655698 path . resolve ( clientProvidedFilePath ) ,
@@ -721,6 +764,7 @@ export class McpContext implements Context {
721764 }
722765
723766 async installExtension ( extensionPath : string ) : Promise < string > {
767+ this . validatePath ( extensionPath ) ;
724768 const id = await this . browser . installExtension ( extensionPath ) ;
725769 return id ;
726770 }
@@ -751,25 +795,29 @@ export class McpContext implements Context {
751795 async getHeapSnapshotAggregates (
752796 filePath : string ,
753797 ) : Promise < Record < string , AggregatedInfoWithUid > > {
798+ this . validatePath ( filePath ) ;
754799 return await this . #heapSnapshotManager. getAggregates ( filePath ) ;
755800 }
756801
757802 async getHeapSnapshotStats (
758803 filePath : string ,
759804 ) : Promise < DevTools . HeapSnapshotModel . HeapSnapshotModel . Statistics > {
805+ this . validatePath ( filePath ) ;
760806 return await this . #heapSnapshotManager. getStats ( filePath ) ;
761807 }
762808
763809 async getHeapSnapshotStaticData (
764810 filePath : string ,
765811 ) : Promise < DevTools . HeapSnapshotModel . HeapSnapshotModel . StaticData | null > {
812+ this . validatePath ( filePath ) ;
766813 return await this . #heapSnapshotManager. getStaticData ( filePath ) ;
767814 }
768815
769816 async getHeapSnapshotNodesByUid (
770817 filePath : string ,
771818 uid : number ,
772819 ) : Promise < DevTools . HeapSnapshotModel . HeapSnapshotModel . ItemsRange > {
820+ this . validatePath ( filePath ) ;
773821 return await this . #heapSnapshotManager. getNodesByUid ( filePath , uid ) ;
774822 }
775823}
0 commit comments