Skip to content

Commit e364baf

Browse files
committed
Auto-merge upstream openclaw/openclaw
2 parents 9a4bb0b + 3b9e0da commit e364baf

8 files changed

Lines changed: 229 additions & 3 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Docs: https://docs.openclaw.ai
4545
- Exec approvals/display: escape raw control characters (including newline and carriage return) in the shared and macOS approval-prompt command sanitizers, so trailing command payloads no longer render on hidden extra lines in the approval UI. (#68198)
4646
- Telegram/streaming: fence same-session stale preview and finalization work after aborts so Telegram no longer replays an older reply or flushes a hidden short preview after the abort confirmation lands. (#68100) Thanks @rubencu.
4747
- OpenAI Codex/OAuth + Pi: keep imported Codex CLI OAuth bootstrap, Pi auth export, and runtime overlay handling aligned so Codex sessions survive refresh and health checks without leaking transient CLI state into saved auth files. Thanks @vincentkoc.
48+
- Config/redact: add `browser.cdpUrl` and `browser.profiles.*.cdpUrl` to sensitive URL config paths so embedded credentials (query tokens and HTTP Basic auth) are properly redacted in `config.get` API responses and availability error messages. (#67679) Thanks @Ziy1-Tan.
4849
- Agents/TTS: report failed speech synthesis as a real tool error so unconfigured providers no longer feed successful TTS failure output back into agent loops. (#67980) Thanks @lawrence3699.
4950
- Gateway/wake: allow unknown properties on wake payloads so external senders like Paperclip can attach opaque metadata without failing schema validation. (#68355) Thanks @kagura-agent.
5051
- Matrix: honor `channels.matrix.network.dangerouslyAllowPrivateNetwork` when creating clients for private-network homeservers. (#68332) Thanks @kagura-agent.

extensions/browser/src/browser/server-context.availability.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
PROFILE_POST_RESTART_WS_TIMEOUT_MS,
88
resolveCdpReachabilityTimeouts,
99
} from "./cdp-timeouts.js";
10+
import { redactCdpUrl } from "./cdp.helpers.js";
1011
import {
1112
closeChromeMcpSession,
1213
ensureChromeMcpAvailable,
@@ -59,6 +60,7 @@ export function createProfileAvailability({
5960
getProfileState,
6061
setProfileRunning,
6162
}: AvailabilityDeps): AvailabilityOps {
63+
const redactedProfileCdpUrl = redactCdpUrl(profile.cdpUrl) ?? profile.cdpUrl;
6264
const capabilities = getBrowserProfileCapabilities(profile);
6365
const resolveTimeouts = (timeoutMs: number | undefined) =>
6466
resolveCdpReachabilityTimeouts({
@@ -210,7 +212,7 @@ export function createProfileAvailability({
210212
if (attachOnly || remoteCdp) {
211213
throw new BrowserProfileUnavailableError(
212214
remoteCdp
213-
? `Remote CDP for profile "${profile.name}" is not reachable at ${profile.cdpUrl}.`
215+
? `Remote CDP for profile "${profile.name}" is not reachable at ${redactedProfileCdpUrl}.`
214216
: `Browser attachOnly is enabled and profile "${profile.name}" is not running.`,
215217
);
216218
}

extensions/browser/src/browser/server-context.ensure-browser-available.waits-for-cdp-ready.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
PROFILE_HTTP_REACHABILITY_TIMEOUT_MS,
66
} from "./cdp-timeouts.js";
77
import * as chromeModule from "./chrome.js";
8+
import { BrowserProfileUnavailableError } from "./errors.js";
89
import { createBrowserRouteContext } from "./server-context.js";
910
import { makeBrowserServerState, mockLaunchedChrome } from "./server-context.test-harness.js";
1011

@@ -175,4 +176,39 @@ describe("browser server-context ensureBrowserAvailable", () => {
175176
expect(launchOpenClawChrome).not.toHaveBeenCalled();
176177
expect(stopOpenClawChrome).not.toHaveBeenCalled();
177178
});
179+
180+
it("redacts credentials in remote CDP availability errors", async () => {
181+
const { launchOpenClawChrome, stopOpenClawChrome } = setupEnsureBrowserAvailableHarness();
182+
const isChromeReachable = vi.mocked(chromeModule.isChromeReachable);
183+
184+
const state = makeBrowserServerState({
185+
profile: {
186+
name: "remote",
187+
cdpUrl: "https://user:pass@browserless.example.com?token=supersecret123",
188+
cdpHost: "browserless.example.com",
189+
cdpIsLoopback: false,
190+
cdpPort: 443,
191+
color: "#00AA00",
192+
driver: "openclaw",
193+
attachOnly: false,
194+
},
195+
resolvedOverrides: {
196+
defaultProfile: "remote",
197+
ssrfPolicy: {},
198+
},
199+
});
200+
const ctx = createBrowserRouteContext({ getState: () => state });
201+
const profile = ctx.forProfile("remote");
202+
203+
isChromeReachable.mockResolvedValue(false);
204+
205+
const promise = profile.ensureBrowserAvailable();
206+
await expect(promise).rejects.toThrow(BrowserProfileUnavailableError);
207+
await expect(promise).rejects.toThrow(
208+
'Remote CDP for profile "remote" is not reachable at https://browserless.example.com/?token=***.',
209+
);
210+
211+
expect(launchOpenClawChrome).not.toHaveBeenCalled();
212+
expect(stopOpenClawChrome).not.toHaveBeenCalled();
213+
});
178214
});

src/config/redact-snapshot.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1162,4 +1162,67 @@ describe("redactConfigSnapshot", () => {
11621162
expect(channels.slack.accounts[0].botToken).toBe(REDACTED_SENTINEL);
11631163
expect(channels.slack.accounts[1].botToken).toBe(REDACTED_SENTINEL);
11641164
});
1165+
1166+
it("redacts browser cdpUrl secrets while preserving bare endpoints", () => {
1167+
const hints = buildConfigSchema().uiHints;
1168+
const raw = `{
1169+
browser: {
1170+
cdpUrl: "https://user:pass@chrome.browserless.io?token=supersecret123",
1171+
profiles: {
1172+
remote: {
1173+
cdpUrl: "https://chrome.staging.example.com?token=staging-secret",
1174+
},
1175+
prod: {
1176+
cdpUrl: "https://alice:secret@chrome.prod.example.com",
1177+
},
1178+
local: {
1179+
cdpUrl: "ws://localhost:9222",
1180+
},
1181+
},
1182+
},
1183+
}`;
1184+
const snapshot = makeSnapshot(
1185+
{
1186+
browser: {
1187+
cdpUrl: "https://user:pass@chrome.browserless.io?token=supersecret123",
1188+
profiles: {
1189+
remote: {
1190+
cdpUrl: "https://chrome.staging.example.com?token=staging-secret",
1191+
},
1192+
prod: {
1193+
cdpUrl: "https://alice:secret@chrome.prod.example.com",
1194+
},
1195+
local: {
1196+
cdpUrl: "ws://localhost:9222",
1197+
},
1198+
},
1199+
},
1200+
},
1201+
raw,
1202+
);
1203+
1204+
const result = redactConfigSnapshot(snapshot, hints);
1205+
const cfg = result.config as typeof snapshot.config;
1206+
expect(cfg.browser.cdpUrl).toBe(REDACTED_SENTINEL);
1207+
expect(cfg.browser.profiles.remote.cdpUrl).toBe(REDACTED_SENTINEL);
1208+
expect(cfg.browser.profiles.prod.cdpUrl).toBe(REDACTED_SENTINEL);
1209+
expect(cfg.browser.profiles.local.cdpUrl).toBe("ws://localhost:9222");
1210+
expect(result.raw).toContain(REDACTED_SENTINEL);
1211+
expect(result.raw).not.toContain("user:pass@");
1212+
expect(result.raw).not.toContain("supersecret123");
1213+
expect(result.raw).not.toContain("staging-secret");
1214+
expect(result.raw).not.toContain("alice:secret@");
1215+
1216+
const restored = restoreRedactedValues(result.config, snapshot.config, hints);
1217+
expect(restored.browser.cdpUrl).toBe(
1218+
"https://user:pass@chrome.browserless.io?token=supersecret123",
1219+
);
1220+
expect(restored.browser.profiles.remote.cdpUrl).toBe(
1221+
"https://chrome.staging.example.com?token=staging-secret",
1222+
);
1223+
expect(restored.browser.profiles.prod.cdpUrl).toBe(
1224+
"https://alice:secret@chrome.prod.example.com",
1225+
);
1226+
expect(restored.browser.profiles.local.cdpUrl).toBe("ws://localhost:9222");
1227+
});
11651228
});

src/config/schema.base.generated.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23522,7 +23522,7 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
2352223522
"browser.cdpUrl": {
2352323523
label: "Browser CDP URL",
2352423524
help: "Remote CDP websocket URL used to attach to an externally managed browser instance. Use this for centralized browser hosts and keep URL access restricted to trusted network paths.",
23525-
tags: ["advanced"],
23525+
tags: ["advanced", "url-secret"],
2352623526
},
2352723527
"browser.color": {
2352823528
label: "Browser Accent Color",
@@ -23572,7 +23572,7 @@ export const GENERATED_BASE_CONFIG_SCHEMA: BaseConfigSchemaResponse = {
2357223572
"browser.profiles.*.cdpUrl": {
2357323573
label: "Browser Profile CDP URL",
2357423574
help: "Per-profile CDP websocket URL used for explicit remote browser routing by profile name. Use this when profile connections terminate on remote hosts or tunnels.",
23575-
tags: ["storage"],
23575+
tags: ["storage", "url-secret"],
2357623576
},
2357723577
"browser.profiles.*.userDataDir": {
2357823578
label: "Browser Profile User Data Dir",

src/gateway/server.config-patch.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,62 @@ describe("gateway config methods", () => {
157157
expect(res.payload?.config).toBeTruthy();
158158
});
159159

160+
it("redacts browser cdpUrl credentials from config.get responses", async () => {
161+
const { createConfigIO, resetConfigRuntimeState } = await import("../config/config.js");
162+
const configPath = createConfigIO().configPath;
163+
await fs.mkdir(path.dirname(configPath), { recursive: true });
164+
try {
165+
await fs.writeFile(
166+
configPath,
167+
`${JSON.stringify(
168+
{
169+
browser: {
170+
cdpUrl: "https://user:pass@chrome.browserless.io?token=supersecret123",
171+
profiles: {
172+
remote: {
173+
cdpUrl: "https://alice:secret@chrome.remote.example.com?token=profile-secret",
174+
},
175+
local: {
176+
cdpUrl: "ws://127.0.0.1:9222",
177+
},
178+
},
179+
},
180+
},
181+
null,
182+
2,
183+
)}\n`,
184+
"utf-8",
185+
);
186+
resetConfigRuntimeState();
187+
188+
const after = await rpcReq<{
189+
raw?: string | null;
190+
config?: {
191+
browser?: {
192+
cdpUrl?: string;
193+
profiles?: Record<string, { cdpUrl?: string }>;
194+
};
195+
};
196+
}>(requireWs(), "config.get", {});
197+
expect(after.ok).toBe(true);
198+
expect(after.payload?.config?.browser?.cdpUrl).toBe("__OPENCLAW_REDACTED__");
199+
expect(after.payload?.config?.browser?.profiles?.remote?.cdpUrl).toBe(
200+
"__OPENCLAW_REDACTED__",
201+
);
202+
expect(after.payload?.config?.browser?.profiles?.local?.cdpUrl).toBe("ws://127.0.0.1:9222");
203+
if (typeof after.payload?.raw === "string") {
204+
expect(after.payload.raw).toContain("__OPENCLAW_REDACTED__");
205+
expect(after.payload.raw).not.toContain("supersecret123");
206+
expect(after.payload.raw).not.toContain("user:pass@");
207+
expect(after.payload.raw).not.toContain("profile-secret");
208+
expect(after.payload.raw).not.toContain("alice:secret@");
209+
}
210+
} finally {
211+
await fs.rm(configPath, { force: true });
212+
resetConfigRuntimeState();
213+
}
214+
});
215+
160216
it("does not reject config.set for unresolved auth-profile refs outside submitted config", async () => {
161217
const missingEnvVar = `OPENCLAW_MISSING_AUTH_PROFILE_REF_${Date.now()}`;
162218
await writeUnresolvedAuthProfileTokenRef(missingEnvVar);
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
isSensitiveUrlQueryParamName,
4+
isSensitiveUrlConfigPath,
5+
SENSITIVE_URL_HINT_TAG,
6+
hasSensitiveUrlHintTag,
7+
redactSensitiveUrl,
8+
redactSensitiveUrlLikeString,
9+
} from "./redact-sensitive-url.js";
10+
11+
describe("redactSensitiveUrl", () => {
12+
it("redacts userinfo and sensitive query params from valid URLs", () => {
13+
expect(redactSensitiveUrl("https://user:pass@example.com/mcp?token=secret&safe=value")).toBe(
14+
"https://***:***@example.com/mcp?token=***&safe=value",
15+
);
16+
});
17+
18+
it("treats query param names case-insensitively", () => {
19+
expect(redactSensitiveUrl("https://example.com/mcp?Access_Token=secret")).toBe(
20+
"https://example.com/mcp?Access_Token=***",
21+
);
22+
});
23+
24+
it("keeps non-sensitive URLs unchanged", () => {
25+
expect(redactSensitiveUrl("https://example.com/mcp?safe=value")).toBe(
26+
"https://example.com/mcp?safe=value",
27+
);
28+
});
29+
});
30+
31+
describe("redactSensitiveUrlLikeString", () => {
32+
it("redacts invalid URL-like strings", () => {
33+
expect(redactSensitiveUrlLikeString("//user:pass@example.com/mcp?client_secret=secret")).toBe(
34+
"//***:***@example.com/mcp?client_secret=***",
35+
);
36+
});
37+
});
38+
39+
describe("isSensitiveUrlQueryParamName", () => {
40+
it("matches the auth-oriented query params used by MCP SSE config redaction", () => {
41+
expect(isSensitiveUrlQueryParamName("token")).toBe(true);
42+
expect(isSensitiveUrlQueryParamName("refresh_token")).toBe(true);
43+
expect(isSensitiveUrlQueryParamName("safe")).toBe(false);
44+
});
45+
});
46+
47+
describe("sensitive URL config metadata", () => {
48+
it("recognizes config paths that may embed URL secrets", () => {
49+
expect(isSensitiveUrlConfigPath("models.providers.*.baseUrl")).toBe(true);
50+
expect(isSensitiveUrlConfigPath("mcp.servers.remote.url")).toBe(true);
51+
expect(isSensitiveUrlConfigPath("gateway.remote.url")).toBe(false);
52+
});
53+
54+
it("recognizes cdpUrl config paths as sensitive (browser CDP URLs can embed credentials)", () => {
55+
expect(isSensitiveUrlConfigPath("browser.cdpUrl")).toBe(true);
56+
expect(isSensitiveUrlConfigPath("browser.profiles.remote.cdpUrl")).toBe(true);
57+
expect(isSensitiveUrlConfigPath("browser.profiles.staging.cdpUrl")).toBe(true);
58+
});
59+
60+
it("uses an explicit url-secret hint tag", () => {
61+
expect(SENSITIVE_URL_HINT_TAG).toBe("url-secret");
62+
expect(hasSensitiveUrlHintTag({ tags: [SENSITIVE_URL_HINT_TAG] })).toBe(true);
63+
expect(hasSensitiveUrlHintTag({ tags: ["security"] })).toBe(false);
64+
});
65+
});

src/shared/net/redact-sensitive-url.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ export function isSensitiveUrlConfigPath(path: string): boolean {
2525
if (path.endsWith(".baseUrl") || path.endsWith(".httpUrl")) {
2626
return true;
2727
}
28+
if (path.endsWith(".cdpUrl")) {
29+
return true;
30+
}
2831
if (path.endsWith(".request.proxy.url")) {
2932
return true;
3033
}

0 commit comments

Comments
 (0)