@@ -17,7 +17,7 @@ const MODULE_ACCESS_INVALID_CONFIG = "ERR_MODULE_ACCESS_INVALID_CONFIG";
1717const MODULE_ACCESS_OUT_OF_SCOPE = "ERR_MODULE_ACCESS_OUT_OF_SCOPE" ;
1818const MODULE_ACCESS_NATIVE_ADDON = "ERR_MODULE_ACCESS_NATIVE_ADDON" ;
1919
20- const SANDBOX_APP_ROOT = "/app " ;
20+ const SANDBOX_APP_ROOT = "/root " ;
2121const SANDBOX_NODE_MODULES_ROOT = `${ SANDBOX_APP_ROOT } /node_modules` ;
2222
2323const VIRTUAL_DIR_MODE = 0o040755 ;
@@ -80,9 +80,59 @@ function isNativeAddonPath(pathValue: string): boolean {
8080 return pathValue . endsWith ( ".node" ) ;
8181}
8282
83+ function collectOverlayAllowedRoots ( hostNodeModulesRoot : string ) : string [ ] {
84+ const roots = new Set < string > ( [ hostNodeModulesRoot ] ) ;
85+ const symlinkScanRoots = [ hostNodeModulesRoot , path . join ( hostNodeModulesRoot , ".pnpm" , "node_modules" ) ] ;
86+
87+ const addSymlinkTarget = ( entryPath : string ) : void => {
88+ try {
89+ const target = fsSync . realpathSync ( entryPath ) ;
90+ roots . add ( target ) ;
91+ } catch {
92+ // Ignore broken symlinks.
93+ }
94+ } ;
95+
96+ const scanDirForSymlinks = ( scanRoot : string ) : void => {
97+ let entries : fsSync . Dirent [ ] = [ ] ;
98+ try {
99+ entries = fsSync . readdirSync ( scanRoot , { withFileTypes : true } ) ;
100+ } catch {
101+ return ;
102+ }
103+
104+ for ( const entry of entries ) {
105+ const entryPath = path . join ( scanRoot , entry . name ) ;
106+ if ( entry . isSymbolicLink ( ) ) {
107+ addSymlinkTarget ( entryPath ) ;
108+ continue ;
109+ }
110+ if ( entry . isDirectory ( ) && entry . name . startsWith ( "@" ) ) {
111+ let scopedEntries : fsSync . Dirent [ ] = [ ] ;
112+ try {
113+ scopedEntries = fsSync . readdirSync ( entryPath , { withFileTypes : true } ) ;
114+ } catch {
115+ continue ;
116+ }
117+ for ( const scopedEntry of scopedEntries ) {
118+ if ( ! scopedEntry . isSymbolicLink ( ) ) continue ;
119+ addSymlinkTarget ( path . join ( entryPath , scopedEntry . name ) ) ;
120+ }
121+ }
122+ }
123+ } ;
124+
125+ for ( const scanRoot of symlinkScanRoots ) {
126+ scanDirForSymlinks ( scanRoot ) ;
127+ }
128+
129+ return [ ...roots ] ;
130+ }
131+
83132export class ModuleAccessFileSystem implements VirtualFileSystem {
84133 private readonly baseFileSystem ?: VirtualFileSystem ;
85134 private readonly hostNodeModulesRoot : string | null ;
135+ private readonly overlayAllowedRoots : string [ ] ;
86136
87137 constructor ( baseFileSystem : VirtualFileSystem | undefined , options : ModuleAccessOptions ) {
88138 this . baseFileSystem = baseFileSystem ;
@@ -99,11 +149,17 @@ export class ModuleAccessFileSystem implements VirtualFileSystem {
99149 const nodeModulesPath = path . join ( cwd , "node_modules" ) ;
100150 try {
101151 this . hostNodeModulesRoot = fsSync . realpathSync ( nodeModulesPath ) ;
152+ this . overlayAllowedRoots = collectOverlayAllowedRoots ( this . hostNodeModulesRoot ) ;
102153 } catch {
103154 this . hostNodeModulesRoot = null ;
155+ this . overlayAllowedRoots = [ ] ;
104156 }
105157 }
106158
159+ private isWithinAllowedOverlayRoots ( canonicalPath : string ) : boolean {
160+ return this . overlayAllowedRoots . some ( ( root ) => isWithinPath ( canonicalPath , root ) ) ;
161+ }
162+
107163 private isSyntheticPath ( virtualPath : string ) : boolean {
108164 if ( virtualPath === "/" || virtualPath === SANDBOX_APP_ROOT ) {
109165 return true ;
@@ -174,10 +230,13 @@ export class ModuleAccessFileSystem implements VirtualFileSystem {
174230
175231 try {
176232 const canonical = await fs . realpath ( hostPath ) ;
177- if ( ! this . hostNodeModulesRoot || ! isWithinPath ( canonical , this . hostNodeModulesRoot ) ) {
233+ if (
234+ ! this . hostNodeModulesRoot ||
235+ ! this . isWithinAllowedOverlayRoots ( canonical )
236+ ) {
178237 throw createModuleAccessError (
179238 MODULE_ACCESS_OUT_OF_SCOPE ,
180- `resolved path '${ canonical } ' escapes '${ this . hostNodeModulesRoot } '` ,
239+ `resolved path '${ canonical } ' escapes overlay roots rooted at '${ this . hostNodeModulesRoot } '` ,
181240 ) ;
182241 }
183242 if ( isNativeAddonPath ( canonical ) ) {
0 commit comments