Files
NetMesh/domain/terminalPromptSecurity.shared.cjs

121 lines
4.5 KiB
JavaScript
Raw Normal View History

"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 };