Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions packages/cli/src/utils/exec-utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { execSync as _exec, type ExecSyncOptions } from 'child_process';
import { execSync as _exec, execFileSync, type ExecSyncOptions } from 'child_process';
import { fileURLToPath } from 'url';

/**
Expand All @@ -23,7 +23,13 @@ export function execPackage(
options?: Omit<ExecSyncOptions, 'env'> & { env?: Record<string, string> },
): void {
const packageManager = process?.versions?.['bun'] ? 'bunx' : 'npx';
execSync(`${packageManager} ${cmd}`, options);
const [executable, ...args] = cmd.split(' ');
execFileSync(packageManager, [executable, ...args], {
encoding: 'utf-8',
stdio: options?.stdio ?? 'inherit',
env: options?.env ? { ...process.env, ...options.env } : undefined,
...options,
Comment on lines +30 to +31

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.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Trailing ...options spread overrides the merged env, dropping process.env.

Object keys are applied in order, so ...options (line 31) overwrites the explicit env set on line 30. When a caller supplies options.env (e.g. the execPrisma fallback always passes _options.env), the carefully merged { ...process.env, ...options.env } is replaced by the bare options.env, losing PATH/DATABASE_URL/etc. This defeats the env-merge this PR intends.

🛠️ Proposed fix
-    const [executable, ...args] = cmd.split(' ');
-    execFileSync(packageManager, [executable, ...args], {
-        encoding: 'utf-8',
-        stdio: options?.stdio ?? 'inherit',
-        env: options?.env ? { ...process.env, ...options.env } : undefined,
-        ...options,
-    });
+    const args = cmd.split(' ').filter(Boolean);
+    execFileSync(packageManager, args, {
+        encoding: 'utf-8',
+        ...options,
+        stdio: options?.stdio ?? 'inherit',
+        env: options?.env ? { ...process.env, ...options.env } : process.env,
+    });
🧰 Tools
🪛 ast-grep (0.44.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execSync as _exec, execFileSync, type ExecSyncOptions } from 'child_process';
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/utils/exec-utils.ts` around lines 30 - 31, The merged env in
the exec utility is being overwritten by the trailing options spread, so the
process-level variables are lost whenever a caller passes env. Update the
options assembly in exec-utils’ exec helper so the final object preserves the
explicit env merge from options.env with process.env instead of letting
...options replace it; use the execPrisma call path as a sanity check that
_options.env still inherits PATH and other base variables.

});
Comment on lines +26 to +32

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.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP -C3 '\bexec(Package|Prisma)\s*\(' packages/cli/src
rg -nP -C2 '--schema\s+"' packages/cli/src

Repository: zenstackhq/zenstack

Length of output: 4940


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,140p' packages/cli/src/utils/exec-utils.ts | cat -n
printf '\n---\n'
sed -n '70,160p' packages/cli/src/actions/migrate.ts | cat -n
printf '\n---\n'
sed -n '60,120p' packages/cli/src/actions/db.ts | cat -n

Repository: zenstackhq/zenstack

Length of output: 8177


🏁 Script executed:

node - <<'JS'
const { spawnSync } = require('child_process');

for (const env of [
  {},
  { FOO: 'bar' },
  { PATH: process.env.PATH },
]) {
  const r = spawnSync('node', ['-e', 'process.stdout.write("ok")'], {
    env,
    encoding: 'utf8',
  });
  console.log('env keys:', Object.keys(env));
  console.log('status:', r.status);
  console.log('error:', r.error && (r.error.code || r.error.message));
  console.log('stdout:', JSON.stringify(r.stdout));
  console.log('stderr:', JSON.stringify(r.stderr));
  console.log('---');
}
JS

Repository: zenstackhq/zenstack

Length of output: 400


Preserve argv and merge env after spreads.
cmd.split(' ') breaks quoted/space-containing args like --schema "${prismaSchemaFile}", and the current spread order drops process.env, so node/npx can fail with ENOENT when PATH is lost. Pass arguments as an array end-to-end and keep the merged env as the final env value.

🧰 Tools
🪛 ast-grep (0.44.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execSync as _exec, execFileSync, type ExecSyncOptions } from 'child_process';
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/utils/exec-utils.ts` around lines 26 - 32, The command
execution in exec-utils currently splits the cmd string on spaces and rebuilds
env before spreading options, which breaks quoted arguments and can overwrite
the merged environment. Update the execFileSync path in the exec-utils helper to
accept and forward arguments as an array end-to-end instead of using cmd.split('
'), and ensure the env passed to execFileSync is the final merged value after
all spreads so process.env is preserved. Use the existing execFileSync call site
and its options handling to make the change without altering other behavior.

}

/**
Expand Down Expand Up @@ -57,5 +63,10 @@ export function execPrisma(args: string, options?: Omit<ExecSyncOptions, 'env'>
return;
}

execSync(`node "${prismaPath}" ${args}`, _options);
execFileSync('node', [prismaPath, ...args.split(' ')], {

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.

🎯 Functional Correctness | 🔴 Critical | 🏗️ Heavy lift

Same quote/space-splitting hazard as execPackage.

args.split(' ') keeps literal quotes and splits paths with spaces. Since every execPrisma caller builds --schema "${prismaSchemaFile}" (and optionally --name "${options.name}"), the node prisma/build/index.js invocation receives a schema path wrapped in literal " characters, breaking db push/migrate dev. Fix at the source by passing an argument array or quote-aware tokenization (see execPackage comment).

🧰 Tools
🪛 ast-grep (0.44.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execSync as _exec, execFileSync, type ExecSyncOptions } from 'child_process';
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/utils/exec-utils.ts` at line 66, `execPrisma` is incorrectly
tokenizing the command string with `args.split(' ')`, which preserves literal
quotes and breaks schema paths containing spaces. Update `execPrisma` in
`exec-utils.ts` to build and pass a real argument array to `execFileSync` (or
use quote-aware parsing like `execPackage`) so callers such as `db push` and
`migrate dev` receive unquoted values for `--schema` and `--name`.

encoding: 'utf-8',
stdio: _options?.stdio ?? 'inherit',
env: _options?.env ? { ...process.env, ..._options.env } : undefined,
..._options,
});
Comment on lines +69 to +71

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.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Trailing ..._options spread overrides the merged env; process.env is always lost here.

_options.env is always populated (PRISMA_HIDE_UPDATE_MESSAGE), so the line 71 spread overwrites the line 69 merge, leaving the node subprocess with only { PRISMA_HIDE_UPDATE_MESSAGE: '1' } — no PATH, no DATABASE_URL. Prisma will fail to resolve its datasource/binaries.

🛠️ Proposed fix
-    execFileSync('node', [prismaPath, ...args.split(' ')], {
-        encoding: 'utf-8',
-        stdio: _options?.stdio ?? 'inherit',
-        env: _options?.env ? { ...process.env, ..._options.env } : undefined,
-        ..._options,
-    });
+    execFileSync('node', [prismaPath, ...args.split(' ').filter(Boolean)], {
+        encoding: 'utf-8',
+        ..._options,
+        stdio: _options?.stdio ?? 'inherit',
+        env: { ...process.env, ..._options.env },
+    });

(Note: the .filter(Boolean)/split here is only a stopgap; the quote-handling issue flagged on Line 66 still needs a real fix.)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
env: _options?.env ? { ...process.env, ..._options.env } : undefined,
..._options,
});
execFileSync('node', [prismaPath, ...args.split(' ').filter(Boolean)], {
encoding: 'utf-8',
..._options,
stdio: _options?.stdio ?? 'inherit',
env: { ...process.env, ..._options.env },
});
🧰 Tools
🪛 ast-grep (0.44.0)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { execSync as _exec, execFileSync, type ExecSyncOptions } from 'child_process';
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/utils/exec-utils.ts` around lines 69 - 71, The merged env in
exec-utils is being overwritten by the trailing _options spread, so the spawned
node process loses process.env values like PATH and DATABASE_URL. Update the
options assembly in exec-utils so the final env always preserves the merged
environment from _options.env plus process.env, and make sure the spread order
in the function does not replace that computed env value. Use the exec-utils
option-building logic as the fix point and keep PRISMA_HIDE_UPDATE_MESSAGE
included without discarding inherited variables.

}