diff --git a/src/shared/git-credential-guard-provenance.test.ts b/src/shared/git-credential-guard-provenance.test.ts index 11ee6c35d14..8b288d11d09 100644 --- a/src/shared/git-credential-guard-provenance.test.ts +++ b/src/shared/git-credential-guard-provenance.test.ts @@ -98,6 +98,26 @@ describe('restoreUnguardedGitCredentialEnv marker handling', () => { expect(env.GIT_CONFIG_KEY_1).toBe('credential.guiPrompt') }) + // The guard forwards only its scalars and the indexed-config protocol into + // WSLENV -- never the askpass names. Removing a name the guard never added + // strips the user's own WSL forwarding, which is the leak's mirror image. + it('keeps a WSLENV name the guard never forwards, even when it appeared after the guard', () => { + const saved = takeGitCredentialGuardEnv() + try { + const env: Record = { WSLENV: 'MY_VAR/u' } + const pre = captureGitCredentialGuardPreGuardState(env) + Object.assign(env, gitCredentialPromptGuardEnv(env, 'win32') as Record) + recordGitCredentialGuardProvenance(env, pre, { appendedConfig: true, forwardToWsl: true }) + // The user forwards their own askpass into WSL from inside the guarded pane. + env.WSLENV = `${env.WSLENV}:GIT_ASKPASS/p` + + expect(restoreUnguardedGitCredentialEnv(env)).toBe(true) + expect(env.WSLENV).toBe('MY_VAR/u:GIT_ASKPASS/p') + } finally { + restoreGitCredentialGuardEnv(saved) + } + }) + // A marker that names no config base did not append config, so nothing indexed // in this environment is Orca's — including a pair that looks exactly like ours. it('never removes indexed config when the marker claims no config base', () => { diff --git a/src/shared/git-credential-guard-provenance.ts b/src/shared/git-credential-guard-provenance.ts index 71537f40b38..1b61e5fb85d 100644 --- a/src/shared/git-credential-guard-provenance.ts +++ b/src/shared/git-credential-guard-provenance.ts @@ -1,5 +1,6 @@ import { GIT_CREDENTIAL_GUARD_CONFIG_ENTRIES, + GIT_CREDENTIAL_GUARD_WSLENV_SCALAR_KEYS, readValidGitConfigEnvCount } from './git-credential-prompt-env' import { addWslEnvKeys } from './wsl-env' @@ -187,9 +188,11 @@ function restoreWslEnv(env: Record, state: RestoreState): void { if (priorNames.has(name)) { return true } + // Why: the guard never forwards the askpass names, so a WSLENV token it + // never added is the user's even when it postdates the pre-guard snapshot. return !( name === TERMINAL_GIT_CREDENTIAL_GUARD_RESTORE_ENV || - (SCALAR_KEYS as readonly string[]).includes(name) || + (GIT_CREDENTIAL_GUARD_WSLENV_SCALAR_KEYS as readonly string[]).includes(name) || GIT_CONFIG_PROTOCOL_KEY_RE.test(name) ) }) diff --git a/src/shared/git-credential-prompt-env.ts b/src/shared/git-credential-prompt-env.ts index f2f266cd1e6..ac3e9eac8c4 100644 --- a/src/shared/git-credential-prompt-env.ts +++ b/src/shared/git-credential-prompt-env.ts @@ -89,6 +89,15 @@ export const GIT_CREDENTIAL_GUARD_CONFIG_ENTRIES = [ ['credential.guiPrompt', 'false'] ] as const satisfies readonly (readonly [key: string, value: string])[] +/** + * The scalar guard variables the guard forwards into WSL. Provenance reads it so + * restoration removes exactly the names the guard adds to WSLENV and no others. + */ +export const GIT_CREDENTIAL_GUARD_WSLENV_SCALAR_KEYS = [ + 'GIT_TERMINAL_PROMPT', + 'GCM_INTERACTIVE' +] as const + /** * Disable interactive Git credential UI while preserving cached credentials * and caller-provided askpass programs. @@ -115,7 +124,7 @@ export function gitCredentialPromptGuardEnv( readValidGitConfigEnvCount(next) === null ? [] : Object.keys(next).filter((key) => GIT_CONFIG_WSLENV_KEY_RE.test(key)) - addWslEnvKeys(next, ['GIT_TERMINAL_PROMPT', 'GCM_INTERACTIVE', ...configKeys]) + addWslEnvKeys(next, [...GIT_CREDENTIAL_GUARD_WSLENV_SCALAR_KEYS, ...configKeys]) } return next }