Compare commits

...
Author SHA1 Message Date
bincxz 9e45982d0a test(terminal): allow alternateScreen in prompt-security options 2026-09-21 15:58:36 +08:00
bincxz 5fc13a3c63 fix(terminal): exempt vim colon only on the alternate screen
A bare `:` on the primary screen can be `sudo -p ':'`. Vim command mode
uses DEC 1049, so require alternateScreen before treating `:` as ex.
2026-09-21 15:49:23 +08:00
bincxz ced2ba0aa6 fix(terminal): only treat a bare colon as vim command-mode
Codex: exempting every `:`-prefixed line would broadcast into custom
prompts such as `sudo -p ':'`. The hang is the first `:` key; later
`:w` no longer matches the trailing-colon heuristic.
2026-09-21 15:35:40 +08:00
bincxz c91dc6c3e1 fix(terminal): keep broadcast going on vim ex command-line
Broadcast pauses on fail-closed colon prompts so passwords are not
copied to peers. Vim command mode's last line is a bare `:`, which
matched that heuristic. Exempt lines that start with `:` so `:w` and
friends stay in sync; `Password:` is unchanged.
2026-09-21 15:18:39 +08:00
5 changed files with 33 additions and 2 deletions

No files matched your search

+6 -1
View File
@@ -1960,7 +1960,12 @@ const TerminalComponent: React.FC<TerminalProps> = ({
sensitivePromptOutputTailRef.current,
chunk,
);
const promptSecurityOptions = { allowHostStyleGreaterThan: isNetworkDevice };
const promptSecurityOptions = {
allowHostStyleGreaterThan: isNetworkDevice,
alternateScreen: termRef.current
? isTerminalAlternateScreenActive(termRef.current)
: false,
};
if (typeof meta?.pluginPipelineSensitiveInput === "boolean") {
passwordPromptActiveRef.current = meta.pluginPipelineSensitiveInput;
if (meta.pluginPipelineSensitiveInput) {
@@ -168,7 +168,10 @@ test('password-prompt input is consumed before every semantic command callback',
});
test('terminal output treats unknown prompt-shaped input boundaries as sensitive', () => {
assert.match(terminalSource, /const promptSecurityOptions = \{ allowHostStyleGreaterThan: isNetworkDevice \};/);
assert.match(
terminalSource,
/const promptSecurityOptions = \{\s*allowHostStyleGreaterThan: isNetworkDevice,\s*alternateScreen: termRef\.current\s*\? isTerminalAlternateScreenActive\(termRef\.current\)\s*: false,\s*\};/,
);
assert.match(terminalSource, /isUntrustedTerminalInputPrompt\([\s\S]*?promptSecurityOptions/);
assert.match(terminalSource, /passwordPromptActiveRef\.current = true;[\s\S]*?autocompleteCloseRef\.current\?\.\(\);/);
assert.match(terminalSource, /isConfirmedTerminalShellPrompt\([\s\S]*?passwordPromptActiveRef\.current = false;/);
+3
View File
@@ -112,6 +112,9 @@ function isUntrustedTerminalInputPrompt(value, options = {}) {
const prompt = lastLogicalLine(value).trim();
if (!prompt) return false;
if (isSensitiveTerminalChallenge(prompt)) return true;
// Vim command-mode entry on the alternate screen. A bare `:` on the
// primary screen stays fail-closed (`sudo -p ':'`).
if (options.alternateScreen && /^:\s*$/.test(prompt)) return false;
if (!/[::>›»]\s*$/u.test(prompt)) return false;
if (hasTypedInputAfterConfirmedPrompt(prompt, options)) return false;
return !isConfirmedTerminalShellPrompt(prompt, options);
+12
View File
@@ -122,6 +122,16 @@ test('ordinary shell commands ending with a colon are not untrusted input prompt
assert.equal(isUntrustedTerminalInputPrompt('Password: '), true);
});
test('vim ex command-line is not an untrusted input prompt (#3422)', () => {
assert.equal(isUntrustedTerminalInputPrompt(':', { alternateScreen: true }), false);
assert.equal(isUntrustedTerminalInputPrompt(': ', { alternateScreen: true }), false);
// Primary screen: `sudo -p ':'` stays fail-closed.
assert.equal(isUntrustedTerminalInputPrompt(':'), true);
assert.equal(isUntrustedTerminalInputPrompt(':', { alternateScreen: false }), true);
assert.equal(isUntrustedTerminalInputPrompt('Password:'), true);
assert.equal(isUntrustedTerminalInputPrompt('Token: '), true);
});
test('renderer and main-process prompt classifiers stay aligned', () => {
const cases: Array<[
string,
@@ -130,6 +140,8 @@ test('renderer and main-process prompt classifiers stay aligned', () => {
]> = [
['Custom authentication> ', undefined, true],
['Please authenticate: ', undefined, true],
[':', { alternateScreen: true }, false],
[':', undefined, true],
['alice@host:~$ ', undefined, false],
['user@host:~$ lsof -i:', undefined, false],
['Challenge # 1:', undefined, true],
+8
View File
@@ -96,6 +96,11 @@ export function isSensitiveTerminalChallenge(value: string): boolean {
type ConfirmedPromptOptions = {
/** Network-device shells commonly use a bare host name followed by `>`. */
allowHostStyleGreaterThan?: boolean;
/**
* True when the terminal is on the alternate screen (DEC 1049). Used to
* tell vim's command-line `:` from a custom `sudo -p ':'` prompt.
*/
alternateScreen?: boolean;
};
/**
@@ -192,6 +197,9 @@ export function isUntrustedTerminalInputPrompt(
const prompt = lastLogicalLine(value).trim();
if (!prompt) return false;
if (isSensitiveTerminalChallenge(prompt)) return true;
// Vim command-mode entry on the alternate screen. A bare `:` on the
// primary screen stays fail-closed (`sudo -p ':'`).
if (options.alternateScreen && /^:\s*$/.test(prompt)) return false;
if (!/[::>›»]\s*$/u.test(prompt)) return false;
// Mid-command punctuation after a real shell prompt must keep broadcasting
// (#2709). Standalone `Label:` / `Custom>` challenges still fail closed.