mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:10:45 +00:00
fix(status): sanitize pairing recovery details
This commit is contained in:
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {},
|
||||
|
||||
@@ -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.",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -75,6 +75,7 @@ const CONNECT_PAIRING_REQUIRED_REASON_VALUES: ReadonlySet<ConnectPairingRequired
|
||||
"scope-upgrade",
|
||||
"metadata-upgrade",
|
||||
]);
|
||||
const PAIRING_CONNECT_REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
|
||||
|
||||
const PAIRING_CONNECT_REASON_METADATA: Readonly<
|
||||
Record<
|
||||
@@ -203,6 +204,11 @@ function normalizePairingConnectReason(value: unknown): ConnectPairingRequiredRe
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function normalizePairingConnectRequestId(value: unknown): string | undefined {
|
||||
const normalized = normalizeOptionalString(value);
|
||||
return normalized && PAIRING_CONNECT_REQUEST_ID_PATTERN.test(normalized) ? normalized : undefined;
|
||||
}
|
||||
|
||||
export function describePairingConnectRequirement(
|
||||
reason: ConnectPairingRequiredReason | undefined,
|
||||
): string {
|
||||
@@ -240,7 +246,7 @@ export function buildPairingConnectErrorDetails(params: {
|
||||
requestId?: string;
|
||||
remediationHint?: string;
|
||||
}): PairingConnectErrorDetails {
|
||||
const requestId = normalizeOptionalString(params.requestId) ?? undefined;
|
||||
const requestId = normalizePairingConnectRequestId(params.requestId);
|
||||
const remediationHint =
|
||||
normalizeOptionalString(params.remediationHint) ??
|
||||
buildPairingConnectRemediationHint(params.reason);
|
||||
@@ -256,7 +262,7 @@ export function buildPairingConnectCloseReason(params: {
|
||||
reason: ConnectPairingRequiredReason | undefined;
|
||||
requestId?: string;
|
||||
}): string {
|
||||
const requestId = normalizeOptionalString(params.requestId) ?? undefined;
|
||||
const requestId = normalizePairingConnectRequestId(params.requestId);
|
||||
const message = buildPairingConnectErrorMessage(params.reason);
|
||||
return requestId ? `${message} (requestId: ${requestId})` : message;
|
||||
}
|
||||
@@ -267,16 +273,13 @@ export function readPairingConnectErrorDetails(
|
||||
if (readConnectErrorDetailCode(details) !== ConnectErrorDetailCodes.PAIRING_REQUIRED) {
|
||||
return null;
|
||||
}
|
||||
if (!details || typeof details !== "object" || Array.isArray(details)) {
|
||||
return { code: ConnectErrorDetailCodes.PAIRING_REQUIRED };
|
||||
}
|
||||
const raw = details as {
|
||||
reason?: unknown;
|
||||
requestId?: unknown;
|
||||
remediationHint?: unknown;
|
||||
};
|
||||
const reason = normalizePairingConnectReason(raw.reason);
|
||||
const requestId = normalizeOptionalString(raw.requestId) ?? undefined;
|
||||
const requestId = normalizePairingConnectRequestId(raw.requestId);
|
||||
const remediationHint =
|
||||
normalizeOptionalString(raw.remediationHint) ?? buildPairingConnectRemediationHint(reason);
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user