Skip to content

Commit 72122c9

Browse files
committed
fix: use execFile in cloneRepo to avoid shell interpolation of user input
cloneRepo previously interpolated projectName into shell command strings via exec(). While isValidName restricts to [a-zA-Z0-9_], this switches to execFile (no shell) for defense-in-depth. Only the git checkout with $() substitution still uses shell exec since it has no user input.
1 parent 140a01d commit 72122c9

2 files changed

Lines changed: 26 additions & 6 deletions

File tree

source/operations/cloneRepo.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import { repoUrl } from '../constants/config.js'
22
import { getProjectFolder } from '../utils/utils.js'
3-
import { exec } from './exec.js'
3+
import { exec, execFile } from './exec.js'
44

55
export async function cloneRepo(projectName: string): Promise<void> {
66
const projectFolder = getProjectFolder(projectName)
77

8-
await exec(`git clone --depth 1 --no-checkout ${repoUrl} ${projectName}`)
9-
await exec('git fetch --tags', { cwd: projectFolder })
8+
await execFile('git', ['clone', '--depth', '1', '--no-checkout', repoUrl, projectName])
9+
await execFile('git', ['fetch', '--tags'], { cwd: projectFolder })
10+
// Shell required for $() command substitution
1011
await exec('git checkout $(git describe --tags $(git rev-list --tags --max-count=1))', {
1112
cwd: projectFolder,
1213
})
13-
await exec('rm -rf .git', { cwd: projectFolder })
14-
await exec('git init', { cwd: projectFolder })
14+
await execFile('rm', ['-rf', '.git'], { cwd: projectFolder })
15+
await execFile('git', ['init'], { cwd: projectFolder })
1516
}

source/operations/exec.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { exec as nodeExec } from 'node:child_process'
1+
import { exec as nodeExec, execFile as nodeExecFile } from 'node:child_process'
22
import { promisify } from 'node:util'
33

44
const execAsync = promisify(nodeExec)
5+
const execFileAsync = promisify(nodeExecFile)
56

67
export async function exec(command: string, options: { cwd?: string } = {}): Promise<string> {
78
try {
@@ -16,3 +17,21 @@ export async function exec(command: string, options: { cwd?: string } = {}): Pro
1617
throw new Error(message)
1718
}
1819
}
20+
21+
export async function execFile(
22+
file: string,
23+
args: string[],
24+
options: { cwd?: string } = {},
25+
): Promise<string> {
26+
try {
27+
const { stdout } = await execFileAsync(file, args, {
28+
cwd: options.cwd,
29+
})
30+
31+
return stdout.trim()
32+
} catch (error: unknown) {
33+
const execError = error as { stderr?: string; message?: string }
34+
const message = execError.stderr?.trim() || execError.message || 'Unknown error'
35+
throw new Error(message)
36+
}
37+
}

0 commit comments

Comments
 (0)