@@ -307,12 +307,30 @@ export function spawn(
307307 options ?: SpawnOptions | undefined ,
308308 extra ?: SpawnExtra | undefined ,
309309) : SpawnResult {
310- // On Windows with shell: true, use just the command name, not the full path.
311- // When shell: true is used, Windows cmd.exe has issues executing full paths to
312- // .cmd/.bat files (e.g., 'C:\...\pnpm.cmd'), often resulting in ENOENT errors.
313- // Using just the command name (e.g., 'pnpm') allows cmd.exe to find it via PATH.
310+ // Windows cmd.exe command resolution for .cmd/.bat/.ps1 files:
311+ //
312+ // When shell: true is used on Windows with script files (.cmd, .bat, .ps1),
313+ // cmd.exe can have issues executing full paths. The solution is to use just
314+ // the command basename without extension and let cmd.exe find it via PATH.
315+ //
316+ // How cmd.exe resolves commands:
317+ // 1. Searches current directory first
318+ // 2. Then searches each directory in PATH environment variable
319+ // 3. For each directory, tries extensions from PATHEXT (.COM, .EXE, .BAT, .CMD, etc.)
320+ // 4. Executes the first match found
321+ //
322+ // Example: Given 'C:\pnpm\pnpm.cmd' with shell: true
323+ // 1. Extract basename without extension: 'pnpm'
324+ // 2. cmd.exe searches PATH directories for 'pnpm'
325+ // 3. PATHEXT causes it to try 'pnpm.com', 'pnpm.exe', 'pnpm.bat', 'pnpm.cmd', etc.
326+ // 4. Finds and executes 'C:\pnpm\pnpm.cmd'
327+ //
328+ // This approach is consistent with how other tools handle Windows execution:
329+ // - npm's promise-spawn: uses which.sync() to find commands in PATH
330+ // - cross-spawn: spawns cmd.exe with escaped arguments
331+ // - execa: uses cross-spawn under the hood for Windows support
332+ //
314333 // See: https://github.com/nodejs/node/issues/3675
315- // Check for .cmd, .bat, .ps1 extensions that indicate a Windows script.
316334 const shell = getOwn ( options , 'shell' )
317335 const WIN32 = /*@__PURE__ */ require ( './constants/WIN32.js' )
318336 if ( WIN32 && shell && windowsScriptExtRegExp . test ( cmd ) ) {
@@ -337,30 +355,30 @@ export function spawn(
337355 spinnerInstance . stop ( )
338356 }
339357 const npmCliPromiseSpawn = getNpmcliPromiseSpawn ( )
340- const promiseSpawnOptions : PromiseSpawnOptions = {
341- signal : abortSignal ,
358+ // Use __proto__: null to prevent prototype pollution when passing to
359+ // third-party code, Node.js built-ins, or JavaScript built-in methods.
360+ // https://github.com/npm/promise-spawn
361+ // https://github.com/nodejs/node/blob/v24.0.1/lib/child_process.js#L674-L678
362+ const promiseSpawnOpts = {
363+ __proto__ : null ,
342364 cwd : typeof spawnOptions . cwd === 'string' ? spawnOptions . cwd : undefined ,
365+ env : {
366+ __proto__ : null ,
367+ ...process . env ,
368+ ...env ,
369+ } as unknown as NodeJS . ProcessEnv ,
370+ signal : abortSignal ,
343371 stdio : spawnOptions . stdio ,
344372 stdioString,
345373 shell : spawnOptions . shell ,
346374 timeout : spawnOptions . timeout ,
347375 uid : spawnOptions . uid ,
348376 gid : spawnOptions . gid ,
349- // Node includes inherited properties of options.env when it normalizes
350- // it due to backwards compatibility. However, this is a prototype sink and
351- // undesired behavior so to prevent it we spread options.env onto a fresh
352- // object with a null [[Prototype]].
353- // https://github.com/nodejs/node/blob/v24.0.1/lib/child_process.js#L674-L678
354- env : Object . assign (
355- Object . create ( null ) ,
356- process . env ,
357- env ,
358- ) as NodeJS . ProcessEnv ,
359- }
377+ } as unknown as PromiseSpawnOptions
360378 const spawnPromise = npmCliPromiseSpawn (
361379 cmd ,
362380 args ? [ ...args ] : [ ] ,
363- promiseSpawnOptions ,
381+ promiseSpawnOpts ,
364382 extra ,
365383 )
366384 const oldSpawnPromise = spawnPromise
@@ -408,12 +426,8 @@ export function spawnSync(
408426 args ?: string [ ] | readonly string [ ] ,
409427 options ?: SpawnSyncOptions | undefined ,
410428) : SpawnSyncReturns < string | Buffer > {
411- // On Windows with shell: true, use just the command name, not the full path.
412- // When shell: true is used, Windows cmd.exe has issues executing full paths to
413- // .cmd/.bat files (e.g., 'C:\...\pnpm.cmd'), often resulting in ENOENT errors.
414- // Using just the command name (e.g., 'pnpm') allows cmd.exe to find it via PATH.
415- // See: https://github.com/nodejs/node/issues/3675
416- // Check for .cmd, .bat, .ps1 extensions that indicate a Windows script.
429+ // Windows cmd.exe command resolution for .cmd/.bat/.ps1 files:
430+ // See spawn() function above for detailed explanation of this approach.
417431 const shell = getOwn ( options , 'shell' )
418432 const WIN32 = /*@__PURE__ */ require ( './constants/WIN32.js' )
419433 if ( WIN32 && shell && windowsScriptExtRegExp . test ( cmd ) ) {
0 commit comments