fix: seal P1 acceptance startup and raw Git state
This commit is contained in:
@@ -400,7 +400,7 @@ export async function resolveProductionExecutables({ repositoryRoot } = {}) {
|
||||
|
||||
let fallbackGitHooksPath;
|
||||
function ownedFallbackGitHooksPath() {
|
||||
if (!fallbackGitHooksPath) fallbackGitHooksPath = mkdtempSync(join(tmpdir(), `p1-git-hooks-${process.pid}-`));
|
||||
if (!fallbackGitHooksPath) fallbackGitHooksPath = realpathSync(mkdtempSync(join(tmpdir(), `p1-git-hooks-${process.pid}-`)));
|
||||
return fallbackGitHooksPath;
|
||||
}
|
||||
function gitSafeConfig(hooksPath) {
|
||||
@@ -533,6 +533,80 @@ function assertSafeGitRepositoryState(argv, cwd, runRoot, fixedHooksPath) {
|
||||
validateGitDirectoryState(realpathSync(target), runRoot);
|
||||
}
|
||||
}
|
||||
function rawGitCommonDirectory(gitDirectory) {
|
||||
const path = join(gitDirectory, "commondir");
|
||||
if (!existsSync(path)) return gitDirectory;
|
||||
const entry = lstatSync(path);
|
||||
const value = entry.isFile() && !entry.isSymbolicLink() ? readFileSync(path, "utf8") : "";
|
||||
if (!/^[^\0\n\r]+\n?$/.test(value)) throw new Error("unsafe Git repository state: common directory is unsafe");
|
||||
const common = resolve(gitDirectory, value.trimEnd());
|
||||
const commonEntry = lstatSync(common);
|
||||
if (!commonEntry.isDirectory() || commonEntry.isSymbolicLink() || realpathSync(common) !== common) {
|
||||
throw new Error("unsafe Git repository state: common directory is unsafe");
|
||||
}
|
||||
return common;
|
||||
}
|
||||
function rawGitConfigEntries(path, required = false) {
|
||||
if (!existsSync(path)) {
|
||||
if (required) throw new Error("unsafe Git repository state: local config is unavailable");
|
||||
return [];
|
||||
}
|
||||
const entry = lstatSync(path);
|
||||
if (!entry.isFile() || entry.isSymbolicLink()) throw new Error("unsafe Git repository state: local config is unsafe");
|
||||
return parseLocalGitConfig(readFileSync(path, "utf8"));
|
||||
}
|
||||
function unsafeRawGitConfigKey(key) {
|
||||
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)
|
||||
|| /^include(?:if\..+)?\.path$/.test(normalized);
|
||||
}
|
||||
function validateRawGitDirectoryState(gitDirectory) {
|
||||
const common = rawGitCommonDirectory(gitDirectory);
|
||||
const entries = [
|
||||
...rawGitConfigEntries(join(common, "config"), true),
|
||||
...rawGitConfigEntries(join(gitDirectory, "config.worktree")),
|
||||
];
|
||||
if (entries.some(([key]) => unsafeRawGitConfigKey(key))) {
|
||||
throw new Error("unsafe Git repository state: executable local config is present");
|
||||
}
|
||||
for (const hooks of new Set([join(common, "hooks"), join(gitDirectory, "hooks")])) {
|
||||
if (!existsSync(hooks)) continue;
|
||||
const hooksEntry = lstatSync(hooks);
|
||||
if (!hooksEntry.isDirectory() || hooksEntry.isSymbolicLink()) throw new Error("unsafe Git repository state: repository hooks are unsafe");
|
||||
for (const entry of readdirSync(hooks, { withFileTypes: true })) {
|
||||
if (entry.isSymbolicLink() || !entry.isFile() || !entry.name.endsWith(".sample")) {
|
||||
throw new Error("unsafe Git repository state: repository hook is present");
|
||||
}
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
function rawRepositoryGitDirectory(argv, cwd) {
|
||||
if (argv[0] === "--git-dir") return repositoryGitDirectory(argv, cwd);
|
||||
let current = argv[0] === "-C" ? argv[1] : cwd;
|
||||
if (!current) return undefined;
|
||||
current = realpathSync(current);
|
||||
while (true) {
|
||||
if (existsSync(join(current, ".git"))) return repositoryGitDirectory(["-C", current]);
|
||||
const parent = dirname(current);
|
||||
if (parent === current) return undefined;
|
||||
current = parent;
|
||||
}
|
||||
}
|
||||
function assertSafeRawGitRepositoryState(argv, cwd, fixedHooksPath) {
|
||||
assertEmptyHooksDirectory(fixedHooksPath);
|
||||
const gitDirectory = rawRepositoryGitDirectory(argv, cwd);
|
||||
const entries = gitDirectory ? validateRawGitDirectoryState(gitDirectory) : [];
|
||||
const target = exactRemoteTarget(argv, entries);
|
||||
if (target !== undefined) {
|
||||
if (!ownedGitPath(target) || !existsSync(join(target, "config"))) {
|
||||
throw new Error("unsafe Git repository state: remote target is not exact and owned");
|
||||
}
|
||||
validateRawGitDirectoryState(realpathSync(target));
|
||||
}
|
||||
}
|
||||
|
||||
const FIXTURE_GIT_CONFIG = new Map([
|
||||
["user.name", new Set(["P1 Fixture Curator", "P1 Context Curator"])],
|
||||
@@ -847,16 +921,24 @@ export async function runCommand(options) {
|
||||
const allowedTht = activeExecutablePolicy?.thtPath;
|
||||
if (canonical !== allowedGit && canonical !== allowedTht) policyError("command executable is not allowlisted", details());
|
||||
if (!Array.isArray(argv) || argv.some((value) => typeof value !== "string")) policyError("command argv must be a string array", details());
|
||||
const guardedByProduction = productionSurfaceOwner !== undefined;
|
||||
let rawGitHooksPath; let rawGitArgv;
|
||||
try {
|
||||
if (canonical === allowedGit) validateGitInvocation(argv, { runRoot: activeExecutablePolicy?.runRoot });
|
||||
if (canonical === allowedGit) {
|
||||
validateGitInvocation(argv, { runRoot: activeExecutablePolicy?.runRoot });
|
||||
if (!guardedByProduction) {
|
||||
rawGitHooksPath = ownedFallbackGitHooksPath();
|
||||
rawGitArgv = argv[0] === "-c" ? argv.slice(2) : argv;
|
||||
assertSafeRawGitRepositoryState(rawGitArgv, cwd, rawGitHooksPath);
|
||||
}
|
||||
}
|
||||
if (canonical === allowedTht) validateThtInvocation(argv, { thtPath: allowedTht, runRoot: activeExecutablePolicy.runRoot, cwd });
|
||||
} catch (error) { policyError(error.message, details()); }
|
||||
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 300_000 || !Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 1 || maxOutputBytes > MAX_OUTPUT) {
|
||||
policyError("command bounds are invalid", details());
|
||||
}
|
||||
return await new Promise((resolvePromise, reject) => {
|
||||
const guardedByProduction = productionSurfaceOwner !== undefined;
|
||||
const childArgv = canonical === allowedGit && !guardedByProduction ? hardenedGitArgv(argv, ownedFallbackGitHooksPath()) : argv;
|
||||
const childArgv = canonical === allowedGit && !guardedByProduction ? hardenedGitArgv(rawGitArgv, rawGitHooksPath) : argv;
|
||||
const childEnv = canonical === allowedGit && !guardedByProduction ? { ...(env ?? {}), ...baseSafeGitEnvironment(canonical) } : env;
|
||||
const child = mutableChildProcess.execFile(canonical, childArgv, { cwd, env: childEnv, timeout: timeoutMs, maxBuffer: maxOutputBytes, encoding: "utf8", shell: false }, (error, stdout, stderr) => {
|
||||
const code = error && typeof error.code === "number" ? error.code : error ? 1 : 0;
|
||||
|
||||
Reference in New Issue
Block a user