fix(security): detect obfuscated commands that bypass allowlist filters (#24287)

* security(exec): add obfuscated command detector

* test(exec): cover obfuscation detector patterns

* security(exec): enforce obfuscation approval on gateway host

* security(exec): enforce obfuscation approval on node host

* test(exec): prevent obfuscation timeout bypass

* chore(changelog): credit obfuscation security fix
This commit is contained in:
Vincent Koc
2026-02-23 02:50:06 -05:00
committed by GitHub
parent 7568ae52ce
commit 0e28e50b45
6 changed files with 422 additions and 9 deletions

View File

@@ -11,7 +11,9 @@ import {
resolveExecApprovals,
resolveExecApprovalsFromFile,
} from "../infra/exec-approvals.js";
import { detectCommandObfuscation } from "../infra/exec-obfuscation-detect.js";
import { buildNodeShellCommand } from "../infra/node-shell.js";
import { logInfo } from "../logger.js";
import { requestExecApprovalDecisionForHost } from "./bash-tools.exec-approval-request.js";
import {
DEFAULT_APPROVAL_TIMEOUT_MS,
@@ -133,12 +135,20 @@ export async function executeNodeHostCommand(
// Fall back to requiring approval if node approvals cannot be fetched.
}
}
const requiresAsk = requiresExecApproval({
ask: hostAsk,
security: hostSecurity,
analysisOk,
allowlistSatisfied,
});
const obfuscation = detectCommandObfuscation(params.command);
if (obfuscation.detected) {
logInfo(
`exec: obfuscation detected (node=${nodeQuery ?? "default"}): ${obfuscation.reasons.join(", ")}`,
);
params.warnings.push(`⚠️ Obfuscated command detected: ${obfuscation.reasons.join("; ")}`);
}
const requiresAsk =
requiresExecApproval({
ask: hostAsk,
security: hostSecurity,
analysisOk,
allowlistSatisfied,
}) || obfuscation.detected;
const invokeTimeoutMs = Math.max(
10_000,
(typeof params.timeoutSec === "number" ? params.timeoutSec : params.defaultTimeoutSec) * 1000 +
@@ -203,7 +213,9 @@ export async function executeNodeHostCommand(
if (decision === "deny") {
deniedReason = "user-denied";
} else if (!decision) {
if (askFallback === "full") {
if (obfuscation.detected) {
deniedReason = "approval-timeout (obfuscation-detected)";
} else if (askFallback === "full") {
approvedByAsk = true;
approvalDecision = "allow-once";
} else if (askFallback === "allowlist") {