From 8b1d526789e63693fc8398b031be892c81477e0e Mon Sep 17 00:00:00 2001 From: Ivorisnoob Date: Thu, 14 May 2026 12:30:14 +0530 Subject: [PATCH 1/2] fix(security): remove shell:true for native exe spawns on Windows ssh, tailscale, git, and powershell.exe are native executables and do not need cmd.exe as an intermediary. Passing shell:true caused Node.js to route args through `cmd.exe /d /s /c "..."`, where metacharacters like & and | are interpreted as command separators. A user-controlled hostname in the SSH flow could trigger arbitrary command execution. npm-installed CLIs (claude, codex, cursor) are intentionally left with shell:true as they may only exist as .cmd wrappers on Windows. --- apps/server/src/diagnostics/ProcessDiagnostics.ts | 2 +- apps/server/src/project/Layers/RepositoryIdentityResolver.ts | 4 ++-- apps/server/src/terminal/Layers/Manager.ts | 2 +- packages/ssh/src/command.ts | 2 +- packages/ssh/src/tunnel.ts | 2 +- packages/tailscale/src/tailscale.ts | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/server/src/diagnostics/ProcessDiagnostics.ts b/apps/server/src/diagnostics/ProcessDiagnostics.ts index 7730b8c7d6b..331010f9c1d 100644 --- a/apps/server/src/diagnostics/ProcessDiagnostics.ts +++ b/apps/server/src/diagnostics/ProcessDiagnostics.ts @@ -280,7 +280,7 @@ const runProcess = Effect.fn("runProcess")( const child = yield* spawner.spawn( ChildProcess.make(input.command, input.args, { cwd: process.cwd(), - shell: process.platform === "win32", + shell: false, }), ); const [stdout, stderr, exitCode] = yield* Effect.all( diff --git a/apps/server/src/project/Layers/RepositoryIdentityResolver.ts b/apps/server/src/project/Layers/RepositoryIdentityResolver.ts index 926c1d0c2ec..09f2ab1e912 100644 --- a/apps/server/src/project/Layers/RepositoryIdentityResolver.ts +++ b/apps/server/src/project/Layers/RepositoryIdentityResolver.ts @@ -94,7 +94,7 @@ const resolveRepositoryIdentityCacheKey = Effect.fn("resolveRepositoryIdentityCa command: "git", args: ["-C", cwd, "rev-parse", "--show-toplevel"], timeoutBehavior: "timedOutResult", - shell: process.platform === "win32", + shell: false, }) .pipe(Effect.option); if (topLevelResult._tag === "None" || topLevelResult.value.code !== 0) { @@ -119,7 +119,7 @@ const resolveRepositoryIdentityFromCacheKey = Effect.fn("resolveRepositoryIdenti command: "git", args: ["-C", cacheKey, "remote", "-v"], timeoutBehavior: "timedOutResult", - shell: process.platform === "win32", + shell: false, }) .pipe(Effect.option); if (remoteResult._tag === "None" || remoteResult.value.code !== 0) { diff --git a/apps/server/src/terminal/Layers/Manager.ts b/apps/server/src/terminal/Layers/Manager.ts index 9f20ebc8315..e8fdcb94f1e 100644 --- a/apps/server/src/terminal/Layers/Manager.ts +++ b/apps/server/src/terminal/Layers/Manager.ts @@ -380,7 +380,7 @@ function checkWindowsSubprocessActivity( timeout: "1500 millis", maxOutputBytes: 32_768, outputMode: "truncate", - shell: process.platform === "win32", + shell: false, timeoutBehavior: "timedOutResult", }); }).pipe( diff --git a/packages/ssh/src/command.ts b/packages/ssh/src/command.ts index dc8839378cd..cb81bbb0f6d 100644 --- a/packages/ssh/src/command.ts +++ b/packages/ssh/src/command.ts @@ -180,7 +180,7 @@ const runSshCommandInScope = Effect.fn("ssh/command.runSshCommand.inScope")(func .spawn( ChildProcess.make("ssh", args, { env: environment, - shell: process.platform === "win32", + shell: false, stdin: { stream: stdinStream(input.stdin), endOnDone: true, diff --git a/packages/ssh/src/tunnel.ts b/packages/ssh/src/tunnel.ts index 5ee5c684779..7b3c4c9c8ce 100644 --- a/packages/ssh/src/tunnel.ts +++ b/packages/ssh/src/tunnel.ts @@ -1153,7 +1153,7 @@ const startSshTunnel = Effect.fn("ssh/tunnel.startSshTunnel")(function* (input: .spawn( ChildProcess.make("ssh", args, { env: childEnvironment, - shell: process.platform === "win32", + shell: false, stdin: { stream: Stream.empty, endOnDone: true, diff --git a/packages/tailscale/src/tailscale.ts b/packages/tailscale/src/tailscale.ts index c40cd54fc44..b00ed82577c 100644 --- a/packages/tailscale/src/tailscale.ts +++ b/packages/tailscale/src/tailscale.ts @@ -135,7 +135,7 @@ export const readTailscaleStatus: Effect.Effect< const child = yield* spawner .spawn( ChildProcess.make("tailscale", args, { - shell: process.platform === "win32", + shell: false, }), ) .pipe( @@ -214,7 +214,7 @@ const runTailscaleCommand = ( const child = yield* spawner .spawn( ChildProcess.make("tailscale", args, { - shell: process.platform === "win32", + shell: false, }), ) .pipe( From c78bc3bebd294f94499b142c6cfbd8046745735c Mon Sep 17 00:00:00 2001 From: Ivorisnoob Date: Thu, 14 May 2026 12:55:13 +0530 Subject: [PATCH 2/2] Reverted Some --- apps/server/src/diagnostics/ProcessDiagnostics.ts | 2 +- apps/server/src/project/Layers/RepositoryIdentityResolver.ts | 4 ++-- apps/server/src/terminal/Layers/Manager.ts | 2 +- packages/tailscale/src/tailscale.ts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/server/src/diagnostics/ProcessDiagnostics.ts b/apps/server/src/diagnostics/ProcessDiagnostics.ts index 331010f9c1d..7730b8c7d6b 100644 --- a/apps/server/src/diagnostics/ProcessDiagnostics.ts +++ b/apps/server/src/diagnostics/ProcessDiagnostics.ts @@ -280,7 +280,7 @@ const runProcess = Effect.fn("runProcess")( const child = yield* spawner.spawn( ChildProcess.make(input.command, input.args, { cwd: process.cwd(), - shell: false, + shell: process.platform === "win32", }), ); const [stdout, stderr, exitCode] = yield* Effect.all( diff --git a/apps/server/src/project/Layers/RepositoryIdentityResolver.ts b/apps/server/src/project/Layers/RepositoryIdentityResolver.ts index 09f2ab1e912..926c1d0c2ec 100644 --- a/apps/server/src/project/Layers/RepositoryIdentityResolver.ts +++ b/apps/server/src/project/Layers/RepositoryIdentityResolver.ts @@ -94,7 +94,7 @@ const resolveRepositoryIdentityCacheKey = Effect.fn("resolveRepositoryIdentityCa command: "git", args: ["-C", cwd, "rev-parse", "--show-toplevel"], timeoutBehavior: "timedOutResult", - shell: false, + shell: process.platform === "win32", }) .pipe(Effect.option); if (topLevelResult._tag === "None" || topLevelResult.value.code !== 0) { @@ -119,7 +119,7 @@ const resolveRepositoryIdentityFromCacheKey = Effect.fn("resolveRepositoryIdenti command: "git", args: ["-C", cacheKey, "remote", "-v"], timeoutBehavior: "timedOutResult", - shell: false, + shell: process.platform === "win32", }) .pipe(Effect.option); if (remoteResult._tag === "None" || remoteResult.value.code !== 0) { diff --git a/apps/server/src/terminal/Layers/Manager.ts b/apps/server/src/terminal/Layers/Manager.ts index e8fdcb94f1e..9f20ebc8315 100644 --- a/apps/server/src/terminal/Layers/Manager.ts +++ b/apps/server/src/terminal/Layers/Manager.ts @@ -380,7 +380,7 @@ function checkWindowsSubprocessActivity( timeout: "1500 millis", maxOutputBytes: 32_768, outputMode: "truncate", - shell: false, + shell: process.platform === "win32", timeoutBehavior: "timedOutResult", }); }).pipe( diff --git a/packages/tailscale/src/tailscale.ts b/packages/tailscale/src/tailscale.ts index b00ed82577c..c40cd54fc44 100644 --- a/packages/tailscale/src/tailscale.ts +++ b/packages/tailscale/src/tailscale.ts @@ -135,7 +135,7 @@ export const readTailscaleStatus: Effect.Effect< const child = yield* spawner .spawn( ChildProcess.make("tailscale", args, { - shell: false, + shell: process.platform === "win32", }), ) .pipe( @@ -214,7 +214,7 @@ const runTailscaleCommand = ( const child = yield* spawner .spawn( ChildProcess.make("tailscale", args, { - shell: false, + shell: process.platform === "win32", }), ) .pipe(