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
121 lines
4.5 KiB
JavaScript
121 lines
4.5 KiB
JavaScript
"use strict";
|
||
|
||
const ESCAPE_SEQUENCE = "\\x" + "1b";
|
||
const BELL_SEQUENCE = "\\x" + "07";
|
||
const ANSI_CONTROL_PATTERN = new RegExp(`${ESCAPE_SEQUENCE}\\[[0-?]*[ -/]*[@-~]`, "gu");
|
||
const OSC_CONTROL_PATTERN = new RegExp(
|
||
`${ESCAPE_SEQUENCE}\\][^${BELL_SEQUENCE}]*(?:${BELL_SEQUENCE}|${ESCAPE_SEQUENCE}\\\\)`,
|
||
"gu",
|
||
);
|
||
const MAX_PROMPT_SECURITY_TAIL_CHARS = 2_048;
|
||
|
||
const SENSITIVE_ENGLISH_LABEL = [
|
||
String.raw`pass(?:word|phrase|code)`,
|
||
String.raw`one[\s-]?time(?:\s+(?:password|passcode|code|token))?`,
|
||
String.raw`\botp\b`,
|
||
String.raw`verification(?:\s+(?:code|token|passcode))?`,
|
||
String.raw`authentication\s+(?:code|token|passcode)`,
|
||
String.raw`security\s+(?:code|token|passcode|pin)`,
|
||
String.raw`\bpin\b`,
|
||
String.raw`\btoken\b`,
|
||
String.raw`2fa`,
|
||
String.raw`two[\s-]?factor`,
|
||
String.raw`multi[\s-]?factor`,
|
||
String.raw`\bmfa\b`,
|
||
String.raw`second\s+factor`,
|
||
String.raw`secondary(?:\s+\w+){0,3}\s+passw(?:ord)?`,
|
||
String.raw`second(?:\s+\w+){0,3}\s+passw(?:ord)?`,
|
||
String.raw`additional(?:\s+\w+){0,3}\s+passw(?:ord)?`,
|
||
String.raw`re[-\s]?enter\s+passw(?:ord)?`,
|
||
String.raw`confirm\s+passw(?:ord)?`,
|
||
String.raw`\bedr\b`,
|
||
String.raw`\bduo\b`,
|
||
].join("|");
|
||
|
||
const SENSITIVE_CJK_LABEL = [
|
||
"密码", "口令", "动态", "一次性", "验证码", "验证信息", "令牌", "双因素", "多因素",
|
||
"短信验证", "手机验证", "二次", "安全密码", "挑战码",
|
||
].join("|");
|
||
|
||
const SENSITIVE_LABEL_PATTERN = new RegExp(
|
||
`(?:${SENSITIVE_ENGLISH_LABEL}|${SENSITIVE_CJK_LABEL})`,
|
||
"iu",
|
||
);
|
||
|
||
function stripTerminalControlSequences(value) {
|
||
return String(value || "").replace(OSC_CONTROL_PATTERN, "").replace(ANSI_CONTROL_PATTERN, "");
|
||
}
|
||
|
||
function lastLogicalLine(value) {
|
||
const plain = stripTerminalControlSequences(value);
|
||
const boundary = Math.max(plain.lastIndexOf("\n"), plain.lastIndexOf("\r"));
|
||
return plain.slice(boundary + 1).slice(-MAX_PROMPT_SECURITY_TAIL_CHARS);
|
||
}
|
||
|
||
function isSensitiveTerminalChallenge(value) {
|
||
const line = lastLogicalLine(value).trim();
|
||
if (!line) return false;
|
||
const label = SENSITIVE_LABEL_PATTERN.exec(line);
|
||
if (!label) return false;
|
||
const prefix = line.slice(0, label.index || 0).trim();
|
||
const suffix = line.slice((label.index || 0) + label[0].length);
|
||
if (suffix.trim().length === 0) {
|
||
return prefix.length === 0
|
||
|| /(?:^|\s)(?:enter|input|provide|type|scan|please|your|current|new|old)\s*$/iu.test(prefix)
|
||
|| /(?:输入|请输入|请)\s*$/u.test(prefix);
|
||
}
|
||
if (/^\s+(?:for|of)\s+[^\r\n]{1,96}$/iu.test(suffix)) return true;
|
||
return /^[^\r\n::>›»]{0,96}[::>›»]\s*[^\r\n]{0,1024}$/u.test(suffix);
|
||
}
|
||
|
||
function isConfirmedTerminalShellPrompt(promptText, options = {}) {
|
||
const prompt = lastLogicalLine(promptText).trim();
|
||
if (!prompt || isSensitiveTerminalChallenge(prompt)) return false;
|
||
if (/[❯❮→➜➤⟩»›]/u.test(prompt)) return true;
|
||
for (const character of prompt) {
|
||
const code = character.charCodeAt(0);
|
||
if (code >= 0xE000 && code <= 0xF8FF) return true;
|
||
}
|
||
if (/[$#%]$/u.test(prompt)) return true;
|
||
if (!prompt.endsWith(">")) return false;
|
||
if (/^(?:PS\s+)?[A-Za-z]:[\\/].*>$/u.test(prompt)) return true;
|
||
if (/[@\\/~:]\S*>$/u.test(prompt)) return true;
|
||
return options.allowHostStyleGreaterThan === true
|
||
&& /^[A-Za-z0-9_.-]+(?:\([^)]{1,128}\))?>$/u.test(prompt);
|
||
}
|
||
|
||
function isPlausibleShellPromptBody(body) {
|
||
const trimmed = body.trim();
|
||
if (!trimmed) return true;
|
||
if (/@|[/\\~:]/u.test(trimmed)) return true;
|
||
if (/^(?:bash|zsh|sh|fish|ksh|csh|tcsh|dash)(?:-[\d.]+)?$/iu.test(trimmed)) return true;
|
||
return /^[a-z0-9_.-]+$/u.test(trimmed);
|
||
}
|
||
|
||
function hasTypedInputAfterConfirmedPrompt(line, options = {}) {
|
||
for (let i = 0; i < line.length - 1; i += 1) {
|
||
const ch = line[i];
|
||
if (ch !== "$" && ch !== "#" && ch !== "%" && ch !== ">") continue;
|
||
const rest = line.slice(i + 1);
|
||
if (!/^\s+\S/u.test(rest)) continue;
|
||
const candidate = line.slice(0, i + 1);
|
||
if (!isConfirmedTerminalShellPrompt(candidate, options)) continue;
|
||
if ((ch === "$" || ch === "#" || ch === "%") && !isPlausibleShellPromptBody(candidate.slice(0, -1))) {
|
||
continue;
|
||
}
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function isUntrustedTerminalInputPrompt(value, options = {}) {
|
||
const prompt = lastLogicalLine(value).trim();
|
||
if (!prompt) return false;
|
||
if (isSensitiveTerminalChallenge(prompt)) return true;
|
||
if (!/[::>›»]\s*$/u.test(prompt)) return false;
|
||
if (hasTypedInputAfterConfirmedPrompt(prompt, options)) return false;
|
||
return !isConfirmedTerminalShellPrompt(prompt, options);
|
||
}
|
||
|
||
module.exports = { isUntrustedTerminalInputPrompt };
|