Skip to content

Commit 4b59878

Browse files
Ziy1-Tanhxy91819
andauthored
fix: redact credentials in browser.cdpUrl config paths (#67679)
Merged via squash. Prepared head SHA: 77bc2c5 Co-authored-by: Ziy1-Tan <49604965+Ziy1-Tan@users.noreply.github.com> Co-authored-by: hxy91819 <8814856+hxy91819@users.noreply.github.com> Reviewed-by: @hxy91819
1 parent c778562 commit 4b59878

8 files changed

Lines changed: 171 additions & 3 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai
77
### Changes
88

99
- macOS/gateway: add `screen.snapshot` support for macOS app nodes, including runtime plumbing, default macOS allowlisting, and docs for monitor preview flows. (#67954) Thanks @BunsDev.
10+
- fix: redact credentials in browser.cdpUrl config paths (#67679). Thanks @Ziy1-Tan
1011

1112
### Fixes
1213

@@ -45,6 +46,7 @@ Docs: https://docs.openclaw.ai
4546
- 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)
4647
- 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.
4748
- 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.
49+
- 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.
4850
- 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.
4951
- 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.
5052
- 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);

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ describe("sensitive URL config metadata", () => {
5151
expect(isSensitiveUrlConfigPath("gateway.remote.url")).toBe(false);
5252
});
5353

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+
5460
it("uses an explicit url-secret hint tag", () => {
5561
expect(SENSITIVE_URL_HINT_TAG).toBe("url-secret");
5662
expect(hasSensitiveUrlHintTag({ tags: [SENSITIVE_URL_HINT_TAG] })).toBe(true);

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)