Skip to content

Commit b4350f8

Browse files
fix: replace execSync string interpolation with execFileSync to prevent command injection
execSync with template strings passes user-supplied URL, branch, repo, and subPath values through the shell, allowing an attacker to inject arbitrary shell commands (e.g. a malicious git URL like `foo; rm -rf /`). Switch to execFileSync with explicit argument arrays in git-cache.ts and registry-provider.ts so arguments are never interpreted by the shell. Also replace the remaining execSync rm -rf call with rmSync. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 01f72c7 commit b4350f8

File tree

2 files changed

+9
-9
lines changed

2 files changed

+9
-9
lines changed

src/utils/git-cache.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, rmSync, readdirSync } from 'node:fs';
22
import { join } from 'node:path';
33
import { createHash } from 'node:crypto';
44
import { homedir, tmpdir } from 'node:os';
5-
import { execSync } from 'node:child_process';
5+
import { execFileSync } from 'node:child_process';
66

77
export interface ResolveRepoOptions {
88
branch?: string;
@@ -31,7 +31,7 @@ function isDirEmpty(dir: string): boolean {
3131

3232
function detectDefaultBranch(url: string): string {
3333
try {
34-
const output = execSync(`git ls-remote --symref ${url} HEAD`, {
34+
const output = execFileSync('git', ['ls-remote', '--symref', url, 'HEAD'], {
3535
encoding: 'utf-8',
3636
stdio: ['pipe', 'pipe', 'pipe'],
3737
timeout: 15_000,
@@ -92,7 +92,7 @@ export function resolveRepo(url: string, options: ResolveRepoOptions = {}): Reso
9292

9393
function cloneRepo(url: string, branch: string, dir: string): void {
9494
mkdirSync(dir, { recursive: true });
95-
execSync(`git clone --depth 1 --branch ${branch} ${url} ${dir}`, {
95+
execFileSync('git', ['clone', '--depth', '1', '--branch', branch, url, dir], {
9696
stdio: 'pipe',
9797
});
9898
}

src/utils/registry-provider.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync } from 'node:fs';
1+
import { existsSync, mkdirSync, rmSync, cpSync, readFileSync, writeFileSync } from 'node:fs';
22
import { join, resolve } from 'node:path';
3-
import { execSync } from 'node:child_process';
3+
import { execFileSync } from 'node:child_process';
44

55
export interface RegistryConfig {
66
name: string;
@@ -186,22 +186,22 @@ export class GitHubProvider implements SkillRegistryProvider {
186186
// Clone and extract specific path
187187
const tmpDir = join(targetDir, '.tmp-git-clone');
188188
try {
189-
execSync(`git clone --depth 1 --filter=blob:none --sparse https://github.com/${repo}.git "${tmpDir}"`, { stdio: 'pipe' });
190-
execSync(`git -C "${tmpDir}" sparse-checkout set "${subPath}"`, { stdio: 'pipe' });
189+
execFileSync('git', ['clone', '--depth', '1', '--filter=blob:none', '--sparse', `https://github.com/${repo}.git`, tmpDir], { stdio: 'pipe' });
190+
execFileSync('git', ['-C', tmpDir, 'sparse-checkout', 'set', subPath], { stdio: 'pipe' });
191191
const skillName = subPath.split('/').pop()!;
192192
const skillDir = join(targetDir, skillName);
193193
mkdirSync(skillDir, { recursive: true });
194194
cpSync(join(tmpDir, subPath), skillDir, { recursive: true });
195195
} finally {
196196
if (existsSync(tmpDir)) {
197-
execSync(`rm -rf "${tmpDir}"`, { stdio: 'pipe' });
197+
rmSync(tmpDir, { recursive: true, force: true });
198198
}
199199
}
200200
} else {
201201
// Clone entire repo as a skill
202202
const skillName = repo.split('/').pop()!;
203203
const skillDir = join(targetDir, skillName);
204-
execSync(`git clone --depth 1 https://github.com/${repo}.git "${skillDir}"`, { stdio: 'pipe' });
204+
execFileSync('git', ['clone', '--depth', '1', `https://github.com/${repo}.git`, skillDir], { stdio: 'pipe' });
205205
}
206206
}
207207
}

0 commit comments

Comments
 (0)