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
+19
-9
@@ -1,9 +1,14 @@
|
||||
/**
|
||||
* git authentication via GIT_ASKPASS.
|
||||
*
|
||||
* a localhost HTTP server serves tokens via single-use UUID codes.
|
||||
* each $git() call writes a unique askpass script with the server
|
||||
* port+code baked into the file body — no secrets in subprocess env.
|
||||
* a localhost HTTP server serves tokens via UUID codes whose lifetime is
|
||||
* bounded by the parent $git() invocation: register() makes the code active,
|
||||
* the script (and any sibling subprocess — e.g. git-lfs pre-push) can fetch
|
||||
* the token any number of times, and $git()'s finally calls revoke() to
|
||||
* close the window. each $git() call writes a unique askpass script with
|
||||
* the server port+code baked into the file body — no secrets in subprocess
|
||||
* env. a replay of a revoked code trips a 409 and revokes the underlying
|
||||
* github installation token.
|
||||
*
|
||||
* see wiki/askpass.md for full security documentation.
|
||||
*/
|
||||
@@ -88,9 +93,13 @@ export function setGitAuthServer(server: GitAuthServer): void {
|
||||
* a remote and need credentials. working-tree operations (checkout, merge)
|
||||
* use $() from shell.ts which has no token.
|
||||
*
|
||||
* per call: registers a one-time code with the auth server, writes a
|
||||
* unique askpass script with port+code baked in, spawns git with
|
||||
* GIT_ASKPASS pointing to the script, and deletes the script in finally.
|
||||
* per call: registers a code with the auth server (valid for the lifetime
|
||||
* of this invocation), writes a unique askpass script with port+code baked
|
||||
* in, spawns git with GIT_ASKPASS pointing to the script. on completion,
|
||||
* revokes the code and deletes the script in finally. multiple sibling
|
||||
* askpass calls within one invocation (e.g. git itself + git-lfs pre-push)
|
||||
* all see a valid code; replay attempts after finally trip a 409 and the
|
||||
* server revokes the underlying github token as a tamper signal.
|
||||
*
|
||||
* @example
|
||||
* await $git("fetch", ["origin", "main"], { token });
|
||||
@@ -149,8 +158,8 @@ export async function $git(
|
||||
});
|
||||
|
||||
if (result.stderr.includes("askpass-compromised")) {
|
||||
log.info("askpass code was already consumed — token has been revoked");
|
||||
throw new Error("git auth failed — askpass code was already consumed, token revoked");
|
||||
log.info("askpass code was replayed after revoke — token has been revoked");
|
||||
throw new Error("git auth failed — askpass code was replayed after revoke, token revoked");
|
||||
}
|
||||
|
||||
if (result.exitCode !== 0) {
|
||||
@@ -175,10 +184,11 @@ export async function $git(
|
||||
stderr: result.stderr.trim(),
|
||||
};
|
||||
} finally {
|
||||
authServer.revoke(code);
|
||||
try {
|
||||
unlinkSync(scriptPath);
|
||||
} catch {
|
||||
// script may have self-deleted already
|
||||
// script may already be gone (e.g. tmpdir cleanup raced us)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user