diff --git a/src/commands/status.command.ts b/src/commands/status.command.ts index 840803cf080..72ccf0c423b 100644 --- a/src/commands/status.command.ts +++ b/src/commands/status.command.ts @@ -1,9 +1,11 @@ import { withProgress } from "../cli/progress.js"; import { + normalizePairingConnectRequestId, readPairingConnectErrorDetails, type ConnectPairingRequiredReason, } from "../gateway/protocol/connect-error-details.js"; import { type RuntimeEnv } from "../runtime.js"; +import { sanitizeTerminalText } from "../terminal/safe-text.js"; import { runStatusJsonCommand } from "./status-json-command.ts"; import { buildStatusOverviewSurfaceFromScan } from "./status-overview-surface.ts"; import { @@ -72,22 +74,13 @@ export function resolvePairingRecoveryContext(params: { const structured = readPairingConnectErrorDetails(params.details); if (structured) { return { - requestId: structured.requestId ?? null, + requestId: normalizePairingConnectRequestId(structured.requestId) ?? null, reason: structured.reason ?? null, - remediationHint: structured.remediationHint ?? null, + remediationHint: structured.remediationHint + ? sanitizeTerminalText(structured.remediationHint) + : null, }; } - const sanitizeRequestId = (value: string): string | null => { - const trimmed = value.trim(); - if (!trimmed) { - return null; - } - // Keep CLI guidance injection-safe: allow only compact id characters. - if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(trimmed)) { - return null; - } - return trimmed; - }; const source = [params.error, params.closeReason] .filter((part) => typeof part === "string" && part.trim().length > 0) .join(" "); @@ -96,7 +89,9 @@ export function resolvePairingRecoveryContext(params: { } const requestIdMatch = source.match(/requestId:\s*([^\s)]+)/i); const requestId = - requestIdMatch && requestIdMatch[1] ? sanitizeRequestId(requestIdMatch[1]) : null; + requestIdMatch && requestIdMatch[1] + ? (normalizePairingConnectRequestId(requestIdMatch[1]) ?? null) + : null; return { requestId: requestId || null, reason: null, remediationHint: null }; } diff --git a/src/commands/status.test.ts b/src/commands/status.test.ts index f86ee41f30f..aace9a5326e 100644 --- a/src/commands/status.test.ts +++ b/src/commands/status.test.ts @@ -1306,6 +1306,20 @@ describe("statusCommand", () => { reason: "scope-upgrade", remediationHint: "Review the requested scopes, then approve the pending upgrade.", }); + expect( + resolvePairingRecoveryContext({ + details: { + code: "PAIRING_REQUIRED", + reason: "scope-upgrade", + requestId: "req-structured-789;rm -rf /", + remediationHint: "\u001b[31mReview\nfirst\u001b[0m", + }, + }), + ).toEqual({ + requestId: null, + reason: "scope-upgrade", + remediationHint: "Review\\nfirst", + }); mocks.loadConfig.mockReturnValue({ session: {}, diff --git a/src/gateway/protocol/connect-error-details.test.ts b/src/gateway/protocol/connect-error-details.test.ts index c15d9a12002..65b015aee65 100644 --- a/src/gateway/protocol/connect-error-details.test.ts +++ b/src/gateway/protocol/connect-error-details.test.ts @@ -5,6 +5,7 @@ import { buildPairingConnectErrorMessage, ConnectPairingRequiredReasons, describePairingConnectRequirement, + normalizePairingConnectRequestId, readConnectErrorDetailCode, readConnectErrorRecoveryAdvice, readPairingConnectErrorDetails, @@ -96,4 +97,20 @@ describe("pairing connect details", () => { "pairing required: device is asking for a higher role than currently approved (requestId: req-789)", ); }); + + it("drops request ids that do not match the allowlist", () => { + expect(normalizePairingConnectRequestId("req-123")).toBe("req-123"); + expect(normalizePairingConnectRequestId("req-123;rm -rf /")).toBeUndefined(); + expect( + readPairingConnectErrorDetails({ + code: "PAIRING_REQUIRED", + reason: "scope-upgrade", + requestId: "req-123;rm -rf /", + }), + ).toEqual({ + code: "PAIRING_REQUIRED", + reason: "scope-upgrade", + remediationHint: "Review the requested scopes, then approve the pending upgrade.", + }); + }); }); diff --git a/src/gateway/protocol/connect-error-details.ts b/src/gateway/protocol/connect-error-details.ts index 2c740f209b6..a47d284cfcf 100644 --- a/src/gateway/protocol/connect-error-details.ts +++ b/src/gateway/protocol/connect-error-details.ts @@ -75,6 +75,7 @@ const CONNECT_PAIRING_REQUIRED_REASON_VALUES: ReadonlySet