fix: harden channel status URL redaction

This commit is contained in:
Gustavo Madeira Santana
2026-04-21 11:54:00 -04:00
parent a849ea5ef5
commit d3bc1aace6
4 changed files with 17 additions and 28 deletions

View File

@@ -256,6 +256,7 @@ describe("channelsStatusCommand SecretRef fallback flow", () => {
[
"gateway timeout after 3000ms",
"Gateway target: wss://user:pass@gateway.example.com/socket?token=secret-token&keep=visible",
"Gateway fallback: (wss://fallback-user:fallback-pass@[bad-host/socket?token=fallback-secret&keep=visible)",
"Source: env OPENCLAW_GATEWAY_URL",
].join("\n"),
),
@@ -280,9 +281,13 @@ describe("channelsStatusCommand SecretRef fallback flow", () => {
const payload = JSON.parse(logs.at(-1) ?? "{}");
expect(errors.join("\n")).not.toContain("user:pass");
expect(errors.join("\n")).not.toContain("secret-token");
expect(errors.join("\n")).not.toContain("fallback-user:fallback-pass");
expect(errors.join("\n")).not.toContain("fallback-secret");
expect(payload.error).toContain("Gateway target:");
expect(payload.error).not.toContain("user:pass");
expect(payload.error).not.toContain("secret-token");
expect(payload.error).not.toContain("fallback-user:fallback-pass");
expect(payload.error).not.toContain("fallback-secret");
expect(payload).toEqual(
expect.objectContaining({
gatewayReachable: false,

View File

@@ -9,6 +9,7 @@ import { formatErrorMessage } from "../../infra/errors.js";
import { formatTimeAgo } from "../../infra/format-time/format-relative.ts";
import { listConfiguredChannelIdsForReadOnlyScope } from "../../plugins/channel-plugin-ids.js";
import { defaultRuntime, type RuntimeEnv, writeRuntimeJson } from "../../runtime.js";
import { redactSensitiveUrlLikeString } from "../../shared/net/redact-sensitive-url.js";
import { formatDocsLink } from "../../terminal/links.js";
import { theme } from "../../terminal/theme.js";
import {
@@ -30,36 +31,9 @@ export type ChannelsStatusOptions = {
timeout?: string;
};
const SENSITIVE_GATEWAY_URL_QUERY_KEYS = new Set([
"access_token",
"api_key",
"auth",
"key",
"password",
"secret",
"signature",
"token",
]);
function redactGatewayUrlSecretsInText(text: string): string {
return text.replace(/\b(?:wss?|https?):\/\/[^\s"'<>]+/gi, (rawUrl) => {
try {
const url = new URL(rawUrl);
if (url.username) {
url.username = "redacted";
}
if (url.password) {
url.password = "redacted";
}
for (const key of url.searchParams.keys()) {
if (SENSITIVE_GATEWAY_URL_QUERY_KEYS.has(key.toLowerCase())) {
url.searchParams.set(key, "redacted");
}
}
return url.toString();
} catch {
return rawUrl;
}
return redactSensitiveUrlLikeString(rawUrl);
});
}

View File

@@ -34,12 +34,21 @@ describe("redactSensitiveUrlLikeString", () => {
"//***:***@example.com/mcp?client_secret=***",
);
});
it("redacts protocol URLs that are too malformed to parse", () => {
expect(
redactSensitiveUrlLikeString(
"wss://fallback-user:fallback-pass@[bad-host/socket?token=fallback-secret&keep=visible)",
),
).toBe("wss://***:***@[bad-host/socket?token=***&keep=visible)");
});
});
describe("isSensitiveUrlQueryParamName", () => {
it("matches the auth-oriented query params used by MCP SSE config redaction", () => {
expect(isSensitiveUrlQueryParamName("token")).toBe(true);
expect(isSensitiveUrlQueryParamName("refresh_token")).toBe(true);
expect(isSensitiveUrlQueryParamName("signature")).toBe(true);
expect(isSensitiveUrlQueryParamName("safe")).toBe(false);
});
});

View File

@@ -15,6 +15,7 @@ const SENSITIVE_URL_QUERY_PARAM_NAMES = new Set([
"auth",
"client_secret",
"refresh_token",
"signature",
]);
export function isSensitiveUrlQueryParamName(name: string): boolean {