fix: reject Git diff driver helpers
This commit is contained in:
@@ -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");
|
if (!entry.isFile() || entry.isSymbolicLink()) throw new Error("unsafe Git repository state: local config is unsafe");
|
||||||
return parseLocalGitConfig(readFileSync(path, "utf8"));
|
return parseLocalGitConfig(readFileSync(path, "utf8"));
|
||||||
}
|
}
|
||||||
function unsafeRawGitConfigKey(key) {
|
function unsafeRawGitConfigKey(key, value = "") {
|
||||||
const normalized = key.toLowerCase();
|
const normalized = key.toLowerCase();
|
||||||
return /^filter\..+\.(?:clean|smudge|process|required)$/.test(normalized)
|
if (normalized === "core.fsmonitor" && /^(?:true|false|1|0)$/i.test(String(value).trim())) return false;
|
||||||
|| /^remote\..+\.(?:uploadpack|receivepack)$/.test(normalized)
|
return /^(?:filter|diff)\..+\.(?:clean|smudge|process|required|textconv|external|command)$/.test(normalized)
|
||||||
|| /^(?:core\.(?:hookspath|attributesfile)|diff\.external|interactive\.difffilter)$/.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);
|
|| /^include(?:if\..+)?\.path$/.test(normalized);
|
||||||
}
|
}
|
||||||
function validateRawGitDirectoryState(gitDirectory) {
|
function validateRawGitDirectoryState(gitDirectory) {
|
||||||
@@ -568,7 +572,7 @@ function validateRawGitDirectoryState(gitDirectory) {
|
|||||||
...rawGitConfigEntries(join(common, "config"), true),
|
...rawGitConfigEntries(join(common, "config"), true),
|
||||||
...rawGitConfigEntries(join(gitDirectory, "config.worktree")),
|
...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");
|
throw new Error("unsafe Git repository state: executable local config is present");
|
||||||
}
|
}
|
||||||
for (const hooks of new Set([join(common, "hooks"), join(gitDirectory, "hooks")])) {
|
for (const hooks of new Set([join(common, "hooks"), join(gitDirectory, "hooks")])) {
|
||||||
|
|||||||
@@ -317,6 +317,37 @@ test("raw runCommand rejects a configured clean filter before exact Git add", as
|
|||||||
await assert.rejects(lstat(marker));
|
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", () => {
|
test("safe environment rejects ambient THT and keeps only strict process allowlist plus fixture values", () => {
|
||||||
const safe = buildSafeEnvironment({
|
const safe = buildSafeEnvironment({
|
||||||
|
|||||||
Reference in New Issue
Block a user