-
-
Notifications
You must be signed in to change notification settings - Fork 149
Security: Command injection via unsanitized string concatenation in execPackage and execPrisma #2739
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Security: Command injection via unsanitized string concatenation in execPackage and execPrisma #2739
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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'; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** | ||||||||||||||||||||
|
|
@@ -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
+26
to
+32
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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/srcRepository: 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 -nRepository: 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('---');
}
JSRepository: zenstackhq/zenstack Length of output: 400 Preserve argv and merge env after spreads. 🧰 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. (detect-child-process-typescript) 🤖 Prompt for AI Agents |
||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| /** | ||||||||||||||||||||
|
|
@@ -57,5 +63,10 @@ export function execPrisma(args: string, options?: Omit<ExecSyncOptions, 'env'> | |||||||||||||||||||
| return; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| execSync(`node "${prismaPath}" ${args}`, _options); | ||||||||||||||||||||
| execFileSync('node', [prismaPath, ...args.split(' ')], { | ||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🔴 Critical | 🏗️ Heavy lift Same quote/space-splitting hazard as
🧰 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. (detect-child-process-typescript) 🤖 Prompt for AI Agents |
||||||||||||||||||||
| encoding: 'utf-8', | ||||||||||||||||||||
| stdio: _options?.stdio ?? 'inherit', | ||||||||||||||||||||
| env: _options?.env ? { ...process.env, ..._options.env } : undefined, | ||||||||||||||||||||
| ..._options, | ||||||||||||||||||||
| }); | ||||||||||||||||||||
|
Comment on lines
+69
to
+71
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win Trailing
🛠️ 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 📝 Committable suggestion
Suggested change
🧰 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. (detect-child-process-typescript) 🤖 Prompt for AI Agents |
||||||||||||||||||||
| } | ||||||||||||||||||||
There was a problem hiding this comment.
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
...optionsspread overrides the mergedenv, droppingprocess.env.Object keys are applied in order, so
...options(line 31) overwrites the explicitenvset on line 30. When a caller suppliesoptions.env(e.g. theexecPrismafallback always passes_options.env), the carefully merged{ ...process.env, ...options.env }is replaced by the bareoptions.env, losingPATH/DATABASE_URL/etc. This defeats the env-merge this PR intends.🛠️ Proposed fix
🧰 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