fix(git-remote): resolve relative pull origins against the repo dir, matching git -C

isTrustedLocalOrigin realpath'd a relative origin against process.cwd(),
but `git -C repoPath pull` resolves relative file remotes against the
repo dir — the containment guard was checking a different path than the
one git actually pulls. Resolve relative origins against repoPath before
the realpath containment check. Regression test mutation-checked (fails
on the cwd-based resolution).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-07-22 11:55:24 -07:00
co-authored by Claude Fable 5
parent 421f892703
commit e2fc3df9fe
2 changed files with 25 additions and 2 deletions
+19
View File
@@ -453,6 +453,25 @@ describe('pullRepo', () => {
expect(flagBlock).not.toContain('protocol.file.allow=always');
rmSync(repo, { recursive: true, force: true });
});
test('relative origin resolves against the repo dir (matching `git -C`), not process.cwd()', async () => {
// git resolves a relative file remote against the repo dir (`git -C repo
// pull`), so the containment guard must too. The relative name exists
// under repo (inside the trusted root) but NOT under process.cwd() — a
// cwd-based guard would realpath-fail and stay strict.
const repo = join(FAKE_GIT_DIR, 'pull-rel-origin');
mkdirSync(join(repo, 'rel-origin.git'), { recursive: true });
setMode('local-origin');
setOrigin('rel-origin.git');
await withEnv({ PATH: fakePath() }, async () => {
pullRepo(repo, { allowLocalFileOrigin: true, trustedOriginRoot: FAKE_GIT_DIR });
});
const pullCall = readArgvLog().find(c => c.includes('pull'));
expect(pullCall).toBeDefined();
const flagBlock = pullCall!.slice(2, pullCall!.indexOf('pull'));
expect(flagBlock).toEqual([...GIT_SSRF_FLAGS_LOCAL]);
rmSync(repo, { recursive: true, force: true });
});
});
// ---------------------------------------------------------------------------