fix(askpass): scope code + script lifetime to one $git() call (#841)
* fix(askpass): scope code + script lifetime to one $git() call, not first password prompt LFS pre-push (and any auth-bound sibling subprocess) consumed the single-use code AND triggered the script self-delete, so git's own push call then hit `fatal: cannot exec '/tmp/pullfrog-…/askpass-…js'` and our server treated the legitimate retry as tamper, revoking the installation token. Observed on nteract/nteract#2987 (LFS repo). `gitAuthServer` codes are now `active` until `$git()`'s finally calls `revoke()`; the script no longer self-deletes (finally already unlinks). Replay after revoke still trips 409 + token revocation, which is the realistic exfiltration vector we care about. * fix(askpass): drop wall-clock TTL on active codes Copilot review on #841 noticed the 5-minute CODE_TTL_MS still applied to active codes, which would re-introduce the original LFS failure mode at a different boundary: a large LFS push lasting >5min would hit a 404 mid- call. $git() uses `activityTimeout: 0` precisely because git fetch/push can take arbitrarily long, so any wall-clock TTL on active codes is wrong. Active codes now live until revoke() is called (in $git()'s finally) or the auth server is closed. Revoked codes keep their 60s replay trap. * docs(askpass): purge stale single-use vocabulary; align error message Pullfrog review on #841 surfaced four doc-drift sites that still described the pre-PR single-use model: - wiki/security.md — 3 references (overview prose, bullets, threat-mitigation table) - action/utils/gitAuth.ts — file-level JSDoc + the 409 error message - wiki/askpass.md — error message quoted in the tamper-evident section - action/utils/gitAuthServer.ts — per-prompt invocation comment was ambiguous All updated to match the active|revoked vocabulary; error message is now "askpass code was replayed after revoke, token revoked".
This commit is contained in:
committed by
pullfrog[bot]
parent
fe2746198c
commit
585a5d21cc
@@ -75,8 +75,23 @@ describe("token delivery", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("single-use enforcement (tamper detection)", () => {
|
||||
it("returns 409 on second use of same code", async () => {
|
||||
describe("code lifecycle (tamper detection)", () => {
|
||||
it("returns the token on repeated use while the code is active", async () => {
|
||||
// a single $git() call can produce multiple legitimate askpass requests:
|
||||
// git itself (username + password), git-lfs pre-push hook, custom hooks.
|
||||
// they must all succeed until $git()'s finally calls revoke().
|
||||
const tmp = makeTmpdir();
|
||||
server = await startGitAuthServer(tmp);
|
||||
const code = server.register("ghs_active_test");
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const res = await fetch(`http://127.0.0.1:${server.port}/${code}`);
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.text()).toBe("ghs_active_test");
|
||||
}
|
||||
});
|
||||
|
||||
it("returns 409 after revoke (replay-after-call trap)", async () => {
|
||||
const tmp = makeTmpdir();
|
||||
server = await startGitAuthServer(tmp);
|
||||
const code = server.register("ghs_tamper_test");
|
||||
@@ -84,10 +99,17 @@ describe("single-use enforcement (tamper detection)", () => {
|
||||
const first = await fetch(`http://127.0.0.1:${server.port}/${code}`);
|
||||
expect(first.status).toBe(200);
|
||||
|
||||
const second = await fetch(`http://127.0.0.1:${server.port}/${code}`);
|
||||
expect(second.status).toBe(409);
|
||||
const body = await second.text();
|
||||
expect(body).toBe("compromised");
|
||||
server.revoke(code);
|
||||
|
||||
const replay = await fetch(`http://127.0.0.1:${server.port}/${code}`);
|
||||
expect(replay.status).toBe(409);
|
||||
expect(await replay.text()).toBe("compromised");
|
||||
});
|
||||
|
||||
it("revoke() on an unknown code is a no-op", async () => {
|
||||
const tmp = makeTmpdir();
|
||||
server = await startGitAuthServer(tmp);
|
||||
expect(() => server!.revoke("nonexistent")).not.toThrow();
|
||||
});
|
||||
|
||||
it("each register() call produces an independent code", async () => {
|
||||
|
||||
Reference in New Issue
Block a user