Compare commits

...
3 changed files with 51 additions and 6 deletions

No files matched your search

+8 -3
View File
@@ -8,6 +8,7 @@ const OSC_CONTROL_PATTERN = new RegExp(
"gu",
);
const MAX_PROMPT_SECURITY_TAIL_CHARS = 2_048;
const ALTERNATE_ROW_START_PATTERN = new RegExp(`${ESCAPE_SEQUENCE}\\[[0-9]+;1H`, "gu");
const SENSITIVE_ENGLISH_LABEL = [
String.raw`pass(?:word|phrase|code)`,
@@ -112,9 +113,13 @@ 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;
// Only Vim's bare Ex prompt may use cursor row boundaries. Preserve the
// original text for sensitivity checks and every other prompt classifier.
// A bare ':' on the primary screen stays fail-closed (`sudo -p ':'`).
if (options.alternateScreen) {
const alternatePrompt = lastLogicalLine(value.replace(ALTERNATE_ROW_START_PATTERN, "\n")).trim();
if (/^:\s*$/.test(alternatePrompt)) return false;
}
if (!/[::>›»]\s*$/u.test(prompt)) return false;
if (hasTypedInputAfterConfirmedPrompt(prompt, options)) return false;
return !isConfirmedTerminalShellPrompt(prompt, options);
+35
View File
@@ -154,3 +154,38 @@ test('renderer and main-process prompt classifiers stay aligned', () => {
assert.equal(sharedPromptSecurity.isUntrustedTerminalInputPrompt(prompt, options), expected, prompt);
}
});
test('Vim row-start cursor positioning separates the alternate-screen colon prompt', () => {
// Captured from two real vim.tiny sessions in the native app: Vim moves to
// column one of its command row instead of printing a newline before ':'.
const renderedBody = '\u001b[?1049h-- INSERT --\u001b[1;1HCLEAN_VIM_MARKER';
let tail = appendTerminalPromptSecurityTail('', renderedBody);
tail = appendTerminalPromptSecurityTail(tail, '\u001b[51;');
tail = appendTerminalPromptSecurityTail(tail, '1H:\u001b[?25h');
for (const classify of [isUntrustedTerminalInputPrompt, sharedPromptSecurity.isUntrustedTerminalInputPrompt]) {
assert.equal(classify(tail, { alternateScreen: true }), false);
assert.equal(classify(tail, { alternateScreen: false }), true);
for (const prompt of ['Password:', 'Token: ', 'Custom authentication> ']) {
assert.equal(classify(`${renderedBody}\u001b[51;1H${prompt}`, { alternateScreen: true }), true, prompt);
}
// Moving within a label must not turn a real challenge into a bare colon.
assert.equal(classify('Password\u001b[51;9H:', { alternateScreen: true }), true);
}
});
test('alternate-screen row changes do not discard sensitive prompts in the same output chunk', () => {
for (const classify of [isUntrustedTerminalInputPrompt, sharedPromptSecurity.isUntrustedTerminalInputPrompt]) {
assert.equal(classify('\u001b[5;1HPassword:\u001b[24;1HReady', { alternateScreen: true }), true);
assert.equal(classify('\u001b[5;1HToken:\u001b[24;1HReady', { alternateScreen: true }), true);
for (const label of ['Password:', 'Token: ']) {
for (const status of ['Ready', ':', 'alice@host:~$ ']) {
const chunk = `\u001b[?1049h\u001b[5;1H${label}\u001b[24;1H${status}`;
assert.equal(classify(chunk, { alternateScreen: true }), true, `${label} / ${status}`);
assert.equal(classify(chunk, { alternateScreen: false }), true, `${label} / ${status}`);
}
}
// Non-bare-colon classifications retain the original logical line.
assert.equal(classify('Custom authentication>\u001b[24;1HReady', { alternateScreen: true }), false);
assert.equal(classify('user@host:~$ lsof -i:\u001b[24;1Hmore:', { alternateScreen: true }), false);
}
});
+8 -3
View File
@@ -6,6 +6,7 @@ const OSC_CONTROL_PATTERN = new RegExp(
'gu',
);
const MAX_PROMPT_SECURITY_TAIL_CHARS = 2_048;
const ALTERNATE_ROW_START_PATTERN = new RegExp(`${ESCAPE_SEQUENCE}\\[[0-9]+;1H`, "gu");
const SENSITIVE_ENGLISH_LABEL = [
String.raw`pass(?:word|phrase|code)`,
@@ -197,9 +198,13 @@ 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;
// Only Vim's bare Ex prompt may use cursor row boundaries. Preserve the
// original text for sensitivity checks and every other prompt classifier.
// A bare ':' on the primary screen stays fail-closed (`sudo -p ':'`).
if (options.alternateScreen) {
const alternatePrompt = lastLogicalLine(value.replace(ALTERNATE_ROW_START_PATTERN, "\n")).trim();
if (/^:\s*$/.test(alternatePrompt)) 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.