fix(server): disable repository fsmonitor commands (#4208)

A repository-controlled core.fsmonitor command can execute when the daemon refreshes Git state. Opening or reconciling a malicious checkout therefore allows code execution as the daemon user. Force core.fsmonitor=false on daemon-owned Git invocations before repository config is considered.
This commit is contained in:
Mohamed Boudra
2026-09-02 13:15:51 +02:00
committed by GitHub
parent e34aea206e
commit 708f9260b1
4 changed files with 66 additions and 10 deletions
@@ -227,7 +227,7 @@ async function collectProviderEntries(
async function collectToolEntries(): Promise<DiagnosticEntry[]> {
const [git, gh] = await Promise.all([
checkTool("git", ["--version"]),
checkTool("git", ["-c", "core.fsmonitor=false", "--version"]),
checkTool("gh", ["--version"]),
]);
return [
@@ -16,10 +16,10 @@ vi.mock("child_process", async () => {
const [command, commandArgs] = args;
if (command === "git" && Array.isArray(commandArgs)) {
const normalizedArgs = commandArgs.map((arg) => String(arg));
// `runGitCommand` always prepends `-c core.quotepath=false`; skip it to
// find the actual git subcommand.
// `runGitCommand` always prepends its two config overrides; skip them
// to find the actual git subcommand.
const subcommandIndex =
normalizedArgs[0] === "-c" && normalizedArgs[1] === "core.quotepath=false" ? 2 : 0;
normalizedArgs[0] === "-c" && normalizedArgs[1] === "core.quotepath=false" ? 4 : 0;
const isTrackedTextDiff =
normalizedArgs[subcommandIndex] === "diff" &&
normalizedArgs.includes("HEAD") &&
@@ -0,0 +1,51 @@
import { chmodSync, existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { runGitCommand } from "./run-git-command.js";
const tempDirs: string[] = [];
function makeTempRepo(): string {
const repo = mkdtempSync(path.join(tmpdir(), "paseo-git-fsmonitor-"));
tempDirs.push(repo);
return repo;
}
async function configureFsmonitor(repo: string): Promise<string> {
const hookPath = path.join(repo, "fsmonitor-hook.sh");
const markerPath = `${hookPath}.marker`;
writeFileSync(hookPath, "#!/bin/sh\n: > \"$0.marker\"\nprintf '1\\n'\n");
chmodSync(hookPath, 0o755);
await runGitCommand(["config", "core.fsmonitor", hookPath], { cwd: repo });
return markerPath;
}
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
}
});
describe("runGitCommand fsmonitor isolation", () => {
it("does not execute a repository-configured fsmonitor command", async () => {
const repo = makeTempRepo();
await runGitCommand(["init"], { cwd: repo });
const markerPath = await configureFsmonitor(repo);
await runGitCommand(["status", "--porcelain"], { cwd: repo });
expect(existsSync(markerPath)).toBe(false);
});
it("overrides core.fsmonitor for commands other than status", async () => {
const repo = makeTempRepo();
await runGitCommand(["init"], { cwd: repo });
await configureFsmonitor(repo);
const result = await runGitCommand(["config", "--get", "core.fsmonitor"], { cwd: repo });
expect(result.stdout.trim()).toBe("false");
});
});
+11 -6
View File
@@ -377,12 +377,17 @@ function runGitCommandWithProvenance(
try {
// `core.quotepath=false` makes git emit raw UTF-8 paths instead of
// octal-escaping non-ASCII bytes (e.g. `测试文件.txt` vs `"\346\265\213..."`).
child = spawnProcess("git", ["-c", "core.quotepath=false", ...args], {
cwd: options.cwd,
envOverlay,
shell: false,
stdio: ["ignore", "pipe", "pipe"],
});
// `core.fsmonitor=false` prevents repository config from launching a command.
child = spawnProcess(
"git",
["-c", "core.quotepath=false", "-c", "core.fsmonitor=false", ...args],
{
cwd: options.cwd,
envOverlay,
shell: false,
stdio: ["ignore", "pipe", "pipe"],
},
);
spawnGitCommandTrace(commandTrace, child.pid);
} catch (error) {
rejectSpawnFailure(error);