fix: reject Git diff driver helpers

This commit is contained in:
2026-08-10 00:30:06 +02:00
parent e042692cae
commit c10857405a
2 changed files with 40 additions and 5 deletions
+9 -5
View File
@@ -555,11 +555,15 @@ function rawGitConfigEntries(path, required = false) {
if (!entry.isFile() || entry.isSymbolicLink()) throw new Error("unsafe Git repository state: local config is unsafe");
return parseLocalGitConfig(readFileSync(path, "utf8"));
}
function unsafeRawGitConfigKey(key) {
function unsafeRawGitConfigKey(key, value = "") {
const normalized = key.toLowerCase();
return /^filter\..+\.(?:clean|smudge|process|required)$/.test(normalized)
|| /^remote\..+\.(?:uploadpack|receivepack)$/.test(normalized)
|| /^(?:core\.(?:hookspath|attributesfile)|diff\.external|interactive\.difffilter)$/.test(normalized)
if (normalized === "core.fsmonitor" && /^(?:true|false|1|0)$/i.test(String(value).trim())) return false;
return /^(?:filter|diff)\..+\.(?:clean|smudge|process|required|textconv|external|command)$/.test(normalized)
|| /^(?:remote\..+\.(?:uploadpack|receivepack)|uploadpack\..+|receivepack\..+)$/.test(normalized)
|| /^credential(?:\..+)?\.helper$/.test(normalized)
|| /^(?:core\.(?:hookspath|attributesfile|fsmonitor|sshcommand|askpass|pager|editor|sequence\.editor)|sequence\.editor|interactive\.difffilter|diff\.external|gpg\.program|gpg\.ssh\.program)$/.test(normalized)
|| /^merge\..+\.driver$/.test(normalized)
|| /^(?:pager\..+|alias\..+|difftool\..+\.(?:cmd|path)|mergetool\..+\.(?:cmd|path))$/.test(normalized)
|| /^include(?:if\..+)?\.path$/.test(normalized);
}
function validateRawGitDirectoryState(gitDirectory) {
@@ -568,7 +572,7 @@ function validateRawGitDirectoryState(gitDirectory) {
...rawGitConfigEntries(join(common, "config"), true),
...rawGitConfigEntries(join(gitDirectory, "config.worktree")),
];
if (entries.some(([key]) => unsafeRawGitConfigKey(key))) {
if (entries.some(([key, value]) => unsafeRawGitConfigKey(key, value))) {
throw new Error("unsafe Git repository state: executable local config is present");
}
for (const hooks of new Set([join(common, "hooks"), join(gitDirectory, "hooks")])) {
+31
View File
@@ -317,6 +317,37 @@ test("raw runCommand rejects a configured clean filter before exact Git add", as
await assert.rejects(lstat(marker));
});
test("raw runCommand rejects a diff driver textconv before exact Git show", async () => {
const repositoryRoot = await fakeRepository();
const marker = join(repositoryRoot, "textconv-helper-ran");
const helper = join(repositoryRoot, "textconv-helper");
await execFileAsync("/usr/bin/git", ["init", "--initial-branch=main"], { cwd: repositoryRoot });
await execFileAsync("/usr/bin/git", ["config", "user.name", "P1 Fixture Curator"], { cwd: repositoryRoot });
await execFileAsync("/usr/bin/git", ["config", "user.email", "p1-curator@example.invalid"], { cwd: repositoryRoot });
await writeFile(join(repositoryRoot, ".gitattributes"), "file diff=evil\n");
await execFileAsync("/usr/bin/git", ["add", ".gitattributes"], { cwd: repositoryRoot });
await execFileAsync("/usr/bin/git", ["commit", "-m", "attributes"], { cwd: repositoryRoot });
await writeFile(join(repositoryRoot, "file"), "v1\n");
await execFileAsync("/usr/bin/git", ["add", "file"], { cwd: repositoryRoot });
await execFileAsync("/usr/bin/git", ["commit", "-m", "v1"], { cwd: repositoryRoot });
await writeFile(join(repositoryRoot, "file"), "v2\n");
await execFileAsync("/usr/bin/git", ["add", "file"], { cwd: repositoryRoot });
await execFileAsync("/usr/bin/git", ["commit", "-m", "v2"], { cwd: repositoryRoot });
await writeFile(helper, `#!/bin/sh\nprintf ran > '${marker}'\nexit 0\n`, { mode: 0o700 });
await execFileAsync("/usr/bin/git", ["config", "diff.evil.textconv", `'${helper}'`], { cwd: repositoryRoot });
const emptyHooks = join(repositoryRoot, "registry", "locks", "empty-hooks");
await mkdir(emptyHooks, { recursive: true });
const { gitPath } = await resolveProductionExecutables({
repositoryRoot: await realpath(join(dirname(fileURLToPath(import.meta.url)), "..", "..")),
});
await assert.rejects(
runCommand({ executable: gitPath, argv: ["-c", `core.hooksPath=${emptyHooks}`, "show", "HEAD"], cwd: repositoryRoot, env: process.env }),
/unsafe Git repository state/,
);
await assert.rejects(lstat(marker));
});
test("safe environment rejects ambient THT and keeps only strict process allowlist plus fixture values", () => {
const safe = buildSafeEnvironment({