@@ -53,6 +53,18 @@ const PATCHED_FS_METHODS: ReadonlyArray<keyof typeof FsType> = [
5353
5454/**
5555 * Function that patches the `fs` module to not escape the given roots.
56+ *
57+ * NOTE: internally Node may call back to `fs.*` methods for example
58+ * realpath: https://github.com/nodejs/node/blob/v24.12.0/lib/fs.js#L2927
59+ * https://github.com/nodejs/node/blob/v24.12.0/lib/fs.js#L2951-L2957
60+ *
61+ * writeFile: https://github.com/nodejs/node/blob/v24.12.0/lib/fs.js#L2372
62+ *
63+ * ... many other places.
64+ *
65+ * However in other scenarios such as ESM module resolution it uses internal invocations
66+ * that can not be patched via `fs.*` methods.
67+ *
5668 * @returns a function to undo the patches.
5769 */
5870export function patcher (
@@ -132,103 +144,98 @@ export function patcher(
132144 // fs.lstat
133145 // =========================================================================
134146
135- if ( ! useInternalLstatPatch ) {
136- fs . lstat = function lstat ( ...args : Parameters < typeof FsType . lstat > ) {
137- // preserve error when calling function without required callback
138- if ( typeof args [ args . length - 1 ] !== 'function' ) {
139- return origLstat ( ...args )
140- }
147+ fs . lstat = function lstat ( ...args : Parameters < typeof FsType . lstat > ) {
148+ // preserve error when calling function without required callback
149+ if ( typeof args [ args . length - 1 ] !== 'function' ) {
150+ return origLstat ( ...args )
151+ }
141152
142- const cb = once ( args [ args . length - 1 ] as Function )
153+ const cb = once ( args [ args . length - 1 ] as Function )
143154
144- // override the callback
145- args [ args . length - 1 ] = function lstatCb (
146- err : Error | null ,
147- stats : Stats | BigIntStats
148- ) {
149- if ( err ) return cb ( err )
155+ // override the callback
156+ args [ args . length - 1 ] = function lstatCb (
157+ err : Error | null ,
158+ stats : Stats | BigIntStats
159+ ) {
160+ if ( err ) return cb ( err )
150161
151- if ( ! stats . isSymbolicLink ( ) ) {
152- // the file is not a symbolic link so there is nothing more to do
153- return cb ( null , stats )
154- }
162+ if ( ! stats . isSymbolicLink ( ) ) {
163+ // the file is not a symbolic link so there is nothing more to do
164+ return cb ( null , stats )
165+ }
155166
156- args [ 0 ] = resolvePathLike ( args [ 0 ] )
167+ args [ 0 ] = resolvePathLike ( args [ 0 ] )
157168
158- if ( ! canEscape ( args [ 0 ] ) ) {
159- // the file can not escaped the sandbox so there is nothing more to do
160- return cb ( null , stats )
161- }
169+ if ( ! canEscape ( args [ 0 ] ) ) {
170+ // the file can not escaped the sandbox so there is nothing more to do
171+ return cb ( null , stats )
172+ }
162173
163- return guardedReadLink ( args [ 0 ] , guardedReadLinkCb )
174+ return guardedReadLink ( args [ 0 ] , guardedReadLinkCb )
164175
165- function guardedReadLinkCb ( str : string ) {
166- if ( str != args [ 0 ] ) {
167- // there are one or more hops within the guards so there is nothing more to do
168- return cb ( null , stats )
169- }
176+ function guardedReadLinkCb ( str : string ) {
177+ if ( str != args [ 0 ] ) {
178+ // there are one or more hops within the guards so there is nothing more to do
179+ return cb ( null , stats )
180+ }
170181
171- // there are no hops so lets report the stats of the real file;
172- // we can't use origRealPath here since that function calls lstat internally
173- // which can result in an infinite loop
174- return unguardedRealPath ( args [ 0 ] , unguardedRealPathCb )
182+ // there are no hops so lets report the stats of the real file;
183+ // we can't use origRealPath here since that function calls lstat internally
184+ // which can result in an infinite loop
185+ return unguardedRealPath ( args [ 0 ] , unguardedRealPathCb )
175186
176- function unguardedRealPathCb (
177- err : Error | null ,
178- str ?: string
179- ) {
180- if ( err ) {
181- if ( ( err as any ) . code === 'ENOENT' ) {
182- // broken link so there is nothing more to do
183- return cb ( null , stats )
184- }
185- return cb ( err )
187+ function unguardedRealPathCb ( err : Error | null , str ?: string ) {
188+ if ( err ) {
189+ if ( ( err as any ) . code === 'ENOENT' ) {
190+ // broken link so there is nothing more to do
191+ return cb ( null , stats )
186192 }
187- return origLstat ( str ! , cb )
193+ return cb ( err )
188194 }
195+ return origLstat ( str ! , cb )
189196 }
190197 }
191-
192- origLstat ( ...args )
193198 }
194199
195- fs . lstatSync = function lstatSync (
196- ...args : Parameters < typeof FsType . lstatSync >
197- ) {
198- const stats = origLstatSync ( ...args )
200+ origLstat ( ...args )
201+ }
199202
200- if ( ! stats ?. isSymbolicLink ( ) ) {
201- // the file is not a symbolic link so there is nothing more to do
202- return stats
203- }
203+ fs . lstatSync = function lstatSync (
204+ ... args : Parameters < typeof FsType . lstatSync >
205+ ) {
206+ const stats = origLstatSync ( ... args )
204207
205- args [ 0 ] = resolvePathLike ( args [ 0 ] )
208+ if ( ! stats ?. isSymbolicLink ( ) ) {
209+ // the file is not a symbolic link so there is nothing more to do
210+ return stats
211+ }
206212
207- if ( ! canEscape ( args [ 0 ] ) ) {
208- // the file can not escaped the sandbox so there is nothing more to do
209- return stats
210- }
213+ args [ 0 ] = resolvePathLike ( args [ 0 ] )
211214
212- const guardedReadLink : string = guardedReadLinkSync ( args [ 0 ] )
213- if ( guardedReadLink != args [ 0 ] ) {
214- // there are one or more hops within the guards so there is nothing more to do
215- return stats
216- }
215+ if ( ! canEscape ( args [ 0 ] ) ) {
216+ // the file can not escaped the sandbox so there is nothing more to do
217+ return stats
218+ }
217219
218- try {
219- args [ 0 ] = unguardedRealPathSync ( args [ 0 ] )
220+ const guardedReadLink : string = guardedReadLinkSync ( args [ 0 ] )
221+ if ( guardedReadLink != args [ 0 ] ) {
222+ // there are one or more hops within the guards so there is nothing more to do
223+ return stats
224+ }
220225
221- // there are no hops so lets report the stats of the real file;
222- // we can't use origRealPathSync here since that function calls lstat internally
223- // which can result in an infinite loop
224- return origLstatSync ( ...args )
225- } catch ( err : any ) {
226- if ( err . code === 'ENOENT' ) {
227- // broken link so there is nothing more to do
228- return stats
229- }
230- throw err
226+ try {
227+ args [ 0 ] = unguardedRealPathSync ( args [ 0 ] )
228+
229+ // there are no hops so lets report the stats of the real file;
230+ // we can't use origRealPathSync here since that function calls lstat internally
231+ // which can result in an infinite loop
232+ return origLstatSync ( ...args )
233+ } catch ( err : any ) {
234+ if ( err . code === 'ENOENT' ) {
235+ // broken link so there is nothing more to do
236+ return stats
231237 }
238+ throw err
232239 }
233240 }
234241
@@ -505,9 +512,7 @@ export function patcher(
505512
506513 if ( promisePropertyDescriptor ) {
507514 const promises : typeof fs . promises = { }
508- if ( ! useInternalLstatPatch ) {
509- promises . lstat = util . promisify ( fs . lstat )
510- }
515+ promises . lstat = util . promisify ( fs . lstat )
511516 // NOTE: node core uses the newer realpath function fs.promises.native instead of fs.realPath
512517 promises . realpath = util . promisify ( fs . realpath . native )
513518 promises . readlink = util . promisify ( fs . readlink )
0 commit comments