Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
145 lines
5.7 KiB
TypeScript
145 lines
5.7 KiB
TypeScript
import { createRequire } from 'node:module';
|
||
import assert from 'node:assert/strict';
|
||
import test from 'node:test';
|
||
|
||
import {
|
||
appendTerminalPromptSecurityTail,
|
||
isConfirmedTerminalShellPrompt,
|
||
isSensitiveTerminalChallenge,
|
||
isUntrustedTerminalInputPrompt,
|
||
shouldUsePluginTerminalCompletionProvider,
|
||
} from './terminalPromptSecurity.ts';
|
||
|
||
const require = createRequire(import.meta.url);
|
||
const sharedPromptSecurity = require('./terminalPromptSecurity.shared.cjs') as {
|
||
isUntrustedTerminalInputPrompt: typeof isUntrustedTerminalInputPrompt;
|
||
};
|
||
|
||
test('terminal prompt security recognizes password, MFA, OTP, PIN, and CJK challenges', () => {
|
||
const challenges = [
|
||
'Password: ',
|
||
'Enter passphrase> ',
|
||
'OTP> 123456',
|
||
'One-time password: 123456',
|
||
'Verification code> 123456',
|
||
'Duo passcode: 123456',
|
||
'MFA token: 123456',
|
||
'Authentication code: 123456',
|
||
'Security PIN: 123456',
|
||
'验证码> 123456',
|
||
'动态口令:123456',
|
||
'二次认证密码:123456',
|
||
];
|
||
for (const challenge of challenges) {
|
||
assert.equal(isSensitiveTerminalChallenge(challenge), true, challenge);
|
||
}
|
||
assert.equal(isSensitiveTerminalChallenge('password authentication succeeded'), false);
|
||
assert.equal(isSensitiveTerminalChallenge('echo OTP'), false);
|
||
});
|
||
|
||
test('terminal prompt security combines split output chunks within a bounded logical line', () => {
|
||
let tail = appendTerminalPromptSecurityTail('', '\u001b[33mVerifi');
|
||
tail = appendTerminalPromptSecurityTail(tail, 'cation code\u001b[0m> ');
|
||
assert.equal(isSensitiveTerminalChallenge(tail), true);
|
||
tail = appendTerminalPromptSecurityTail(tail, '\r\nuser@host:~$ ');
|
||
assert.equal(isSensitiveTerminalChallenge(tail), false);
|
||
assert.ok(tail.length <= 2_048);
|
||
});
|
||
|
||
test('terminal prompt security positively confirms shell prompts and fails closed on ambiguous prompts', () => {
|
||
const confirmed = [
|
||
'$ ',
|
||
'root# ',
|
||
'user@host:~/repo$ ',
|
||
'➜ ~/repo ',
|
||
'\uE0B0 ',
|
||
'PS C:\\Users\\alice> ',
|
||
'C:\\Users\\alice> ',
|
||
'user@host> ',
|
||
];
|
||
for (const prompt of confirmed) {
|
||
assert.equal(isConfirmedTerminalShellPrompt(prompt), true, prompt);
|
||
}
|
||
assert.equal(isConfirmedTerminalShellPrompt('OTP> '), false);
|
||
assert.equal(isConfirmedTerminalShellPrompt('custom> '), false);
|
||
assert.equal(isConfirmedTerminalShellPrompt('router> ', { allowHostStyleGreaterThan: true }), true);
|
||
});
|
||
|
||
test('plugin terminal completion policy requires a non-sensitive confirmed shell prompt', () => {
|
||
assert.equal(shouldUsePluginTerminalCompletionProvider({
|
||
sensitiveInputActive: false,
|
||
promptText: 'user@host:~$ ',
|
||
}), true);
|
||
assert.equal(shouldUsePluginTerminalCompletionProvider({
|
||
sensitiveInputActive: true,
|
||
promptText: 'user@host:~$ ',
|
||
}), false);
|
||
assert.equal(shouldUsePluginTerminalCompletionProvider({
|
||
sensitiveInputActive: false,
|
||
promptText: 'OTP> ',
|
||
}), false);
|
||
assert.equal(shouldUsePluginTerminalCompletionProvider({
|
||
sensitiveInputActive: false,
|
||
promptText: 'custom> ',
|
||
}), false);
|
||
});
|
||
|
||
test('unknown prompt-shaped authentication boundaries fail closed', () => {
|
||
assert.equal(isUntrustedTerminalInputPrompt('Custom authentication> '), true);
|
||
assert.equal(isUntrustedTerminalInputPrompt('Please authenticate: '), true);
|
||
assert.equal(isUntrustedTerminalInputPrompt('alice@host:~$ '), false);
|
||
assert.equal(isUntrustedTerminalInputPrompt('router> ', { allowHostStyleGreaterThan: true }), false);
|
||
assert.equal(isUntrustedTerminalInputPrompt('router> '), true);
|
||
});
|
||
|
||
test('ordinary shell commands ending with a colon are not untrusted input prompts (#2709)', () => {
|
||
// Broadcast pauses while passwordPromptActive is sticky. Typing `lsof -i:`
|
||
// must not trip the fail-closed colon heuristic, or peers miss the rest.
|
||
const commandLines = [
|
||
'user@host:~$ lsof -i:',
|
||
'bash-5.2$ lsof -i:',
|
||
'root# lsof -i:',
|
||
'PS C:\\Users\\alice> lsof -i:',
|
||
'C:\\Users\\alice> lsof -i:',
|
||
];
|
||
for (const line of commandLines) {
|
||
assert.equal(isUntrustedTerminalInputPrompt(line), false, line);
|
||
}
|
||
assert.equal(
|
||
isUntrustedTerminalInputPrompt('router> show ip:', { allowHostStyleGreaterThan: true }),
|
||
false,
|
||
);
|
||
// Bare host-style prompts stay fail-closed unless explicitly allowed.
|
||
assert.equal(isUntrustedTerminalInputPrompt('router> show ip:'), true);
|
||
// Mid-label `#` is not a prompt boundary (no whitespace after `#`).
|
||
assert.equal(isUntrustedTerminalInputPrompt('Challenge #1:'), true);
|
||
// Markers inside English challenge labels must not look like a shell PS1.
|
||
assert.equal(isUntrustedTerminalInputPrompt('Challenge # 1:'), true);
|
||
assert.equal(isUntrustedTerminalInputPrompt('Account $ code:'), true);
|
||
// Standalone auth-shaped lines without a confirmed shell prompt still fail closed.
|
||
assert.equal(isUntrustedTerminalInputPrompt('Please authenticate: '), true);
|
||
assert.equal(isUntrustedTerminalInputPrompt('Token: '), true);
|
||
assert.equal(isUntrustedTerminalInputPrompt('Password: '), true);
|
||
});
|
||
|
||
test('renderer and main-process prompt classifiers stay aligned', () => {
|
||
const cases: Array<[
|
||
string,
|
||
{ allowHostStyleGreaterThan?: boolean } | undefined,
|
||
boolean,
|
||
]> = [
|
||
['Custom authentication> ', undefined, true],
|
||
['Please authenticate: ', undefined, true],
|
||
['alice@host:~$ ', undefined, false],
|
||
['user@host:~$ lsof -i:', undefined, false],
|
||
['Challenge # 1:', undefined, true],
|
||
['router> show ip:', { allowHostStyleGreaterThan: true }, false],
|
||
['router> show ip:', undefined, true],
|
||
];
|
||
|
||
for (const [prompt, options, expected] of cases) {
|
||
assert.equal(isUntrustedTerminalInputPrompt(prompt, options), expected, prompt);
|
||
assert.equal(sharedPromptSecurity.isUntrustedTerminalInputPrompt(prompt, options), expected, prompt);
|
||
}
|
||
});
|