Skip to content

Commit f328a5c

Browse files
qozleclaude
andauthored
fix: prevent hang in restoreConfigFromBase on repos with .gitmodules (#1166)
When a PR head contains `.gitmodules`, git's default `fetch.recurseSubmodules=on-demand` config causes `git fetch` to attempt submodule object fetches. In CI (no credentials), this blocks indefinitely waiting for auth — producing ~4-hour hangs reported in #1088. Two changes, both defence-in-depth: 1. Delete SENSITIVE_PATHS *before* fetching. The attacker-controlled `.gitmodules` is absent during the network operation, so git never sees a submodule config to follow regardless of git settings. 2. Pass `--no-recurse-submodules` to the fetch. Suppresses submodule fetching explicitly, independent of any git config on the runner. The original order (fetch-then-delete) was a brief window where `.gitmodules` from the PR head could influence the fetch. Reordering also tightens the security property: if `git checkout` below fails, the attacker-controlled file is already gone rather than present during fetch. Fixes #1088. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b15d475 commit f328a5c

File tree

1 file changed

+20
-10
lines changed

1 file changed

+20
-10
lines changed

src/github/operations/restore-config.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,20 +44,30 @@ export function restoreConfigFromBase(baseBranch: string): void {
4444
`Restoring ${SENSITIVE_PATHS.join(", ")} from origin/${baseBranch} (PR head is untrusted)`,
4545
);
4646

47-
// Fetch base first — if this fails we haven't touched the workspace and the
48-
// caller sees a clean error.
49-
execFileSync("git", ["fetch", "origin", baseBranch, "--depth=1"], {
50-
stdio: "inherit",
51-
env: process.env,
52-
});
53-
54-
// Delete PR-controlled versions. If the restore below fails for a given path,
55-
// that path stays deleted — the safe fallback (no attacker-controlled config).
56-
// A bare `git checkout` alone wouldn't remove files the PR added, so nuke first.
47+
// Delete PR-controlled versions BEFORE fetching so the attacker-controlled
48+
// .gitmodules is absent during the network operation. If git reads .gitmodules
49+
// during fetch (fetch.recurseSubmodules=on-demand, the git default), it will
50+
// attempt to fetch submodule objects and block on credential prompts in CI —
51+
// causing an indefinite hang. Deleting first closes that window.
52+
//
53+
// If the restore below fails for a given path, that path stays deleted —
54+
// the safe fallback (no attacker-controlled config). A bare `git checkout`
55+
// alone wouldn't remove files the PR added, so nuke first.
5756
for (const p of SENSITIVE_PATHS) {
5857
rmSync(p, { recursive: true, force: true });
5958
}
6059

60+
// --no-recurse-submodules: explicitly suppress submodule fetching regardless of
61+
// fetch.recurseSubmodules config. Defense-in-depth alongside the delete above.
62+
execFileSync(
63+
"git",
64+
["fetch", "origin", baseBranch, "--depth=1", "--no-recurse-submodules"],
65+
{
66+
stdio: "inherit",
67+
env: process.env,
68+
},
69+
);
70+
6171
for (const p of SENSITIVE_PATHS) {
6272
try {
6373
execFileSync("git", ["checkout", `origin/${baseBranch}`, "--", p], {

0 commit comments

Comments
 (0)