fix(terminal): restore only the WSLENV names the credential guard forwards

The guard forwards `GIT_TERMINAL_PROMPT`, `GCM_INTERACTIVE` and the indexed
config protocol into WSLENV -- never `GIT_ASKPASS`/`SSH_ASKPASS`. Restoration
removed all four scalars, so a WSLENV token the guard never added was
stripped from an unguarded child purely because it postdated the pre-guard
snapshot, destroying the user's own WSL forwarding. Both sides now read one
exported constant, so the ratchet keeps them coupled.
This commit is contained in:
Brennan Benson
2026-08-28 06:39:05 -07:00
parent e6054e671d
commit 9d2e9e75c6
3 changed files with 34 additions and 2 deletions
@@ -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<string, string> = { WSLENV: 'MY_VAR/u' }
const pre = captureGitCredentialGuardPreGuardState(env)
Object.assign(env, gitCredentialPromptGuardEnv(env, 'win32') as Record<string, string>)
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', () => {
@@ -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<string, string>, 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)
)
})
+10 -1
View File
@@ -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
}