Skip to content
Open
Show file tree
Hide file tree
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
7 changes: 3 additions & 4 deletions src/tools/static-analysis.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// Static analysis runner using native linters and compilers
// Delegates dead code detection to deterministic tools, not LLM guessing

import { exec } from "child_process";
import { execFile } from "child_process";
import { stat } from "fs/promises";
import { resolve, extname } from "path";
import { promisify } from "util";

const execAsync = promisify(exec);
const execFileAsync = promisify(execFile);

export interface StaticAnalysisOptions {
rootDir: string;
Expand All @@ -29,9 +29,8 @@ const LINTER_MAP: Record<string, { cmd: string; args: string[] }> = {
};

async function runCommand(cmd: string, args: string[], cwd: string): Promise<LintResult> {
const fullCmd = `${cmd} ${args.join(" ")}`;
try {
const { stdout, stderr } = await execAsync(fullCmd, { cwd, timeout: 30000, maxBuffer: 1024 * 512 });
const { stdout, stderr } = await execFileAsync(cmd, args, { cwd, timeout: 30000, maxBuffer: 1024 * 512 });
return { tool: cmd, output: (stdout + stderr).trim(), exitCode: 0 };
} catch (err: any) {
return { tool: cmd, output: (err.stdout ?? "") + (err.stderr ?? ""), exitCode: err.code ?? 1 };
Expand Down
102 changes: 102 additions & 0 deletions test/main/static-analysis-injection.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Test: CWE-78 command injection via targetPath in static analysis
// Verifies that shell metacharacters in targetPath cannot be used for injection

import { describe, it, before, after } from "node:test";
import assert from "node:assert";
import { mkdir, writeFile, rm, readFile } from "fs/promises";
import { resolve, join } from "path";

const { runStaticAnalysis } =
await import("../../build/tools/static-analysis.js");

const FIXTURE = resolve("test/_injection_fixtures");
const SENTINEL = join(FIXTURE, "pwned.txt");

before(async () => {
await mkdir(FIXTURE, { recursive: true });
await writeFile(
join(FIXTURE, "safe.py"),
"# Safe python file\n# FEATURE: test\nprint('hello')\n",
);
});

after(async () => {
await rm(FIXTURE, { recursive: true, force: true });
});

describe("CWE-78: command injection via targetPath", () => {
it("should not execute injected commands via $() with .py extension", async () => {
// This payload ends in .py so it matches the Python linter
// The $() will be interpreted by the shell in exec()
const maliciousPath = `$(echo INJECTED > ${SENTINEL}).py`;
try {
await runStaticAnalysis({ rootDir: FIXTURE, targetPath: maliciousPath });
} catch {
// errors are acceptable – injection must not succeed
}

let injected = false;
try {
await readFile(SENTINEL, "utf-8");
injected = true;
} catch {
injected = false;
}
assert.strictEqual(injected, false, "Command injection via $() succeeded – sentinel file was created");
});

it("should not execute injected commands via backticks with .py extension", async () => {
const maliciousPath = "`echo INJECTED > " + SENTINEL + "`.py";
try {
await runStaticAnalysis({ rootDir: FIXTURE, targetPath: maliciousPath });
} catch {
// errors are acceptable – injection must not succeed
}

let injected = false;
try {
await readFile(SENTINEL, "utf-8");
injected = true;
} catch {
injected = false;
}
assert.strictEqual(injected, false, "Command injection via backticks succeeded – sentinel file was created");
});

it("should not execute injected commands via semicolon ending with .py", async () => {
// Craft: foo; echo INJECTED > sentinel; echo.py
const maliciousPath = `foo; echo INJECTED > ${SENTINEL}; echo.py`;
try {
await runStaticAnalysis({ rootDir: FIXTURE, targetPath: maliciousPath });
} catch {
// errors are acceptable – injection must not succeed
}

let injected = false;
try {
await readFile(SENTINEL, "utf-8");
injected = true;
} catch {
injected = false;
}
assert.strictEqual(injected, false, "Command injection via semicolon succeeded – sentinel file was created");
});

it("should not execute injected commands via pipe ending with .py", async () => {
const maliciousPath = `safe.py | tee ${SENTINEL} | cat foo.py`;
try {
await runStaticAnalysis({ rootDir: FIXTURE, targetPath: maliciousPath });
} catch {
// errors are acceptable – injection must not succeed
}

let injected = false;
try {
await readFile(SENTINEL, "utf-8");
injected = true;
} catch {
injected = false;
}
assert.strictEqual(injected, false, "Command injection via pipe succeeded – sentinel file was created");
});
});