Skip to content

Mitigate Windows GH CLI command injection#1874

Open
juliusmarminge wants to merge 3 commits intomainfrom
codex/fix-github-cli-command-injection-vulnerability
Open

Mitigate Windows GH CLI command injection#1874
juliusmarminge wants to merge 3 commits intomainfrom
codex/fix-github-cli-command-injection-vulnerability

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented Apr 10, 2026

Motivation

  • The runProcess helper enabled shell: process.platform === "win32", which routes commands through cmd.exe on Windows and allows shell metacharacters in untrusted gh arguments (branch names, titles, body file paths) to inject commands.
  • The change aims to close this command-injection vector for GitHub CLI invocations while preserving existing behavior for other callers that may rely on shell execution.

Description

  • Add an optional shell?: boolean field to ProcessRunOptions so callers can explicitly control shell use via options.shell.
  • Change runProcess to use shell: options.shell ?? (process.platform === "win32") so the previous Windows default is preserved unless a caller overrides it.
  • Force gh invocations in the GitHub CLI layer to run with shell: false so gh receives arguments directly and cmd.exe cannot interpret untrusted input.

Testing

  • Attempted bun fmt but it could not run in this environment (bun: command not found) so formatting was not verified automatically.
  • Attempted bun lint but it could not run in this environment (bun: command not found) so lint checks were not executed.
  • Attempted bun typecheck but it could not run in this environment (bun: command not found) so typechecks were not executed.

Codex Task


Note

Medium Risk
Touches process execution behavior and security boundaries: runProcess now supports overriding shell usage and GitHub CLI calls explicitly disable shell, which could affect Windows-specific invocation semantics if any callers relied on shell parsing.

Overview
Mitigates a Windows command-injection vector by adding an optional shell flag to runProcess and wiring it into the spawn options (options.shell ?? (process.platform === "win32")).

Updates the GitHub CLI layer (GitHubCli.ts) to always run gh with shell: false, ensuring untrusted arguments are passed directly to the executable rather than interpreted by cmd.exe.

Reviewed by Cursor Bugbot for commit 1fd6244. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Mitigate Windows CLI command injection by disabling shell in GitHub CLI process spawning

  • Adds an optional shell property to the ProcessRunOptions interface in processRunner.ts, letting callers override the default shell behavior.
  • Updates runProcess to use options.shell ?? process.platform === 'win32' so existing callers retain current behavior.
  • Forces shell: false for all GitHub CLI invocations in GitHubCli.ts, preventing shell interpolation of arguments on Windows.
  • Risk: disabling the shell on Windows may break gh invocations that relied on shell path resolution, though this is the intended security trade-off.

Macroscope summarized ce1b679.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 10, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d54c8f1e-cac4-4281-aa46-98dd81182ff5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-github-cli-command-injection-vulnerability

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added size:XS 0-9 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Apr 10, 2026
@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp bot commented Apr 10, 2026

Approvability

Verdict: Needs human review

This security fix changes runtime shell execution behavior on Windows. An open review comment raises a medium-severity concern that shell: false may break GitHub CLI invocations for Windows users with .cmd shim installations, warranting human review to assess the tradeoff between security hardening and compatibility.

You can customize Macroscope's approvability policy. Learn more.

runProcess("gh", input.args, {
cwd: input.cwd,
timeoutMs: input.timeoutMs ?? DEFAULT_TIMEOUT_MS,
shell: false,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shell:false breaks gh .cmd shims on Windows

Medium Severity

Setting shell: false for gh invocations on Windows prevents Node.js spawn from executing .cmd/.bat shims. If gh is installed via a package manager that creates .cmd shims (rather than placing gh.exe directly on PATH), the spawn call will fail with ENOENT. The original shell: true on Windows existed precisely to handle this — the openai/codex repo itself has issue #16337 about this exact pattern. While most gh installations use .exe, this is a regression for some Windows setups.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 1fd6244. Configure here.

- Keep `spawn` shell usage limited to Windows defaults
- Avoid shell-based command execution on other platforms
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Missing parentheses in nullish coalescing expression
    • Added explicit parentheses around process.platform === "win32" to make the nullish coalescing precedence unambiguous.

Create PR

Or push these changes by commenting:

@cursor push 05f0d160a1
Preview (05f0d160a1)
diff --git a/apps/server/src/processRunner.ts b/apps/server/src/processRunner.ts
--- a/apps/server/src/processRunner.ts
+++ b/apps/server/src/processRunner.ts
@@ -140,7 +140,7 @@
       cwd: options.cwd,
       env: options.env,
       stdio: "pipe",
-      shell: options.shell ?? process.platform === "win32",
+      shell: options.shell ?? (process.platform === "win32"),
     });
 
     let stdout = "";

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit ce1b679. Configure here.

env: options.env,
stdio: "pipe",
shell: process.platform === "win32",
shell: options.shell ?? process.platform === "win32",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing parentheses in nullish coalescing expression

Low Severity

The expression options.shell ?? process.platform === "win32" relies on === having higher precedence than ?? to evaluate correctly. While functionally equivalent to options.shell ?? (process.platform === "win32"), the PR description itself explicitly includes the parentheses. Adding them would make the intent unambiguous, especially since this is a security-sensitive change preventing command injection. Developers unfamiliar with ?? precedence may misread this as (options.shell ?? process.platform) === "win32".

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit ce1b679. Configure here.

@cursor
Copy link
Copy Markdown
Contributor

cursor bot commented Apr 10, 2026

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: shell:false breaks gh .cmd shims on Windows
    • Added resolveShellOption() in processRunner.ts that probes PATH on Windows to detect .cmd/.bat shims and uses shell:true only for those, preserving shell:false for .exe binaries to prevent command injection.

Create PR

Or push these changes by commenting:

@cursor push 7885e2ba38
Preview (7885e2ba38)
diff --git a/apps/server/src/processRunner.ts b/apps/server/src/processRunner.ts
--- a/apps/server/src/processRunner.ts
+++ b/apps/server/src/processRunner.ts
@@ -1,4 +1,6 @@
 import { type ChildProcess as ChildProcessHandle, spawn, spawnSync } from "node:child_process";
+import { accessSync } from "node:fs";
+import { delimiter, extname, join } from "node:path";
 
 export interface ProcessRunOptions {
   cwd?: string | undefined;
@@ -82,6 +84,45 @@
 const DEFAULT_MAX_BUFFER_BYTES = 8 * 1024 * 1024;
 
 /**
+ * On Windows, `.cmd` / `.bat` shims (created by npm, Scoop, etc.) can only be
+ * executed by `spawn` when `shell: true`.  When the caller explicitly requests
+ * `shell: false` we still need to honour that for `.exe` binaries, but we must
+ * fall back to `shell: true` when the resolved command is a script shim.
+ */
+function resolveShellOption(command: string, explicit: boolean | undefined): boolean {
+  if (process.platform !== "win32") return explicit ?? false;
+  if (explicit === true) return true;
+
+  if (extname(command)) {
+    const ext = extname(command).toLowerCase();
+    return ext === ".cmd" || ext === ".bat";
+  }
+
+  const pathEnv = process.env["PATH"] ?? process.env["Path"] ?? "";
+  const dirs = pathEnv.split(delimiter).filter(Boolean);
+  for (const dir of dirs) {
+    for (const ext of [".cmd", ".bat"]) {
+      try {
+        accessSync(join(dir, `${command}${ext}`));
+        return true;
+      } catch {
+        // not found, continue
+      }
+    }
+    for (const ext of [".exe", ".com", ""]) {
+      try {
+        accessSync(join(dir, `${command}${ext}`));
+        return false;
+      } catch {
+        // not found, continue
+      }
+    }
+  }
+
+  return explicit ?? false;
+}
+
+/**
  * On Windows with `shell: true`, `child.kill()` only terminates the `cmd.exe`
  * wrapper, leaving the actual command running. Use `taskkill /T` to kill the
  * entire process tree instead.
@@ -140,7 +181,7 @@
       cwd: options.cwd,
       env: options.env,
       stdio: "pipe",
-      shell: options.shell ?? (process.platform === "win32"),
+      shell: resolveShellOption(command, options.shell),
     });
 
     let stdout = "";

You can send follow-ups to the cloud agent here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

aardvark codex size:XS 0-9 changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant