fix(browser): redact chrome bridge diagnostics · openclaw/openclaw@82e8b52
vincentkoc
·
2026-05-17
·
via Recent Commits to openclaw:main
File tree
extensions/browser/src/browser
| Original file line number | Diff line number | Diff line change |
|---|
@@ -80,6 +80,7 @@ Docs: https://docs.openclaw.ai
|
80 | 80 | - Providers/xAI: replace the retired `grok-imagine-image-pro` image model with `grok-imagine-image-quality` in the bundled image-generation provider and docs. (#81399) Thanks @KateWilkins. |
81 | 81 | - Providers/OAuth: let browser-hosted identity provider pages read successful localhost callback responses, preventing xAI Grok OAuth from showing a false connection failure after OpenClaw completes login. |
82 | 82 | - Gateway/security: reject malformed HTTP and WebSocket request targets with the existing auth failure response instead of letting invalid URL parsing crash the Gateway. Fixes GHSA-6hc3-f4rg-377m. |
| 83 | +- Browser/CDP: redact credential-bearing Chrome MCP and managed Chrome launch diagnostics, and require exact loopback entries before treating `NO_PROXY` as already covering local CDP proxy bypasses. |
83 | 84 | - Gateway/diagnostics: redact credential-bearing gateway target URLs and client diagnostics while preserving raw connection URLs for programmatic use, so connect-failure logs no longer surface embedded tokens. |
84 | 85 | - Gateway/auth: honor `OPENCLAW_GATEWAY_TOKEN` as the remote interactive fallback when no remote token is configured, keeping remote TUI setup aligned with documented auth precedence. |
85 | 86 | - Providers/xAI: continue polling video generations while xAI reports in-flight jobs as `pending`, so Grok video requests no longer fail before the final `done` response. (#82610) Thanks @Manzojunior. |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -308,6 +308,29 @@ describe("withNoProxyForLocalhost preserves user-configured NO_PROXY", () => {
|
308 | 308 | delete process.env.no_proxy; |
309 | 309 | } |
310 | 310 | }); |
| 311 | + |
| 312 | +it("does not treat substring matches as complete loopback coverage", async () => { |
| 313 | +const userNoProxy = "notlocalhost,127.0.0.10,[::1].example"; |
| 314 | +process.env.NO_PROXY = userNoProxy; |
| 315 | +process.env.no_proxy = userNoProxy; |
| 316 | +process.env.HTTP_PROXY = "http://proxy:8080"; |
| 317 | + |
| 318 | +try { |
| 319 | +const { withNoProxyForLocalhost } = await import("./cdp-proxy-bypass.js"); |
| 320 | + |
| 321 | +await withNoProxyForLocalhost(async () => { |
| 322 | +expect(process.env.NO_PROXY).toBe(`${userNoProxy},localhost,127.0.0.1,[::1]`); |
| 323 | +expect(process.env.no_proxy).toBe(`${userNoProxy},localhost,127.0.0.1,[::1]`); |
| 324 | +}); |
| 325 | + |
| 326 | +expect(process.env.NO_PROXY).toBe(userNoProxy); |
| 327 | +expect(process.env.no_proxy).toBe(userNoProxy); |
| 328 | +} finally { |
| 329 | +delete process.env.HTTP_PROXY; |
| 330 | +delete process.env.NO_PROXY; |
| 331 | +delete process.env.no_proxy; |
| 332 | +} |
| 333 | +}); |
311 | 334 | }); |
312 | 335 | |
313 | 336 | describe("withNoProxyForCdpUrl", () => { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -47,9 +47,13 @@ const LOOPBACK_ENTRIES = "localhost,127.0.0.1,[::1]";
|
47 | 47 | |
48 | 48 | function noProxyAlreadyCoversLocalhost(): boolean { |
49 | 49 | const current = process.env.NO_PROXY || process.env.no_proxy || ""; |
50 | | -return ( |
51 | | -current.includes("localhost") && current.includes("127.0.0.1") && current.includes("[::1]") |
| 50 | +const entries = new Set( |
| 51 | +current |
| 52 | +.split(",") |
| 53 | +.map((entry) => entry.trim().toLowerCase()) |
| 54 | +.filter(Boolean), |
52 | 55 | ); |
| 56 | +return entries.has("localhost") && entries.has("127.0.0.1") && entries.has("[::1]"); |
53 | 57 | } |
54 | 58 | |
55 | 59 | export async function withNoProxyForLocalhost<T>(fn: () => Promise<T>): Promise<T> { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -230,6 +230,30 @@ describe("chrome MCP page parsing", () => {
|
230 | 230 | ]); |
231 | 231 | }); |
232 | 232 | |
| 233 | +it("redacts remote CDP URL secrets from attach failures", async () => { |
| 234 | +const secretToken = "browserless-secret-token-1234567890"; // pragma: allowlist secret |
| 235 | +const cdpUrl = `wss://browserless.example/chrome?token=${secretToken}`; |
| 236 | + |
| 237 | +let message = ""; |
| 238 | +try { |
| 239 | +await ensureChromeMcpAvailable( |
| 240 | +"remote-profile", |
| 241 | +{ |
| 242 | + cdpUrl, |
| 243 | +mcpCommand: process.execPath, |
| 244 | +mcpArgs: ["-e", "process.exit(1)"], |
| 245 | +}, |
| 246 | +{ ephemeral: true }, |
| 247 | +); |
| 248 | +} catch (err) { |
| 249 | +message = err instanceof Error ? err.message : String(err); |
| 250 | +} |
| 251 | + |
| 252 | +expect(message).toContain("Chrome MCP existing-session attach failed"); |
| 253 | +expect(message).toContain("browserless.example"); |
| 254 | +expect(message).not.toContain(secretToken); |
| 255 | +}); |
| 256 | + |
233 | 257 | it("parses new_page text responses and returns the created tab", async () => { |
234 | 258 | const factory: ChromeMcpSessionFactory = async () => createFakeSession(); |
235 | 259 | setChromeMcpSessionFactoryForTest(factory); |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -8,8 +8,10 @@ import {
|
8 | 8 | readStringValue, |
9 | 9 | } from "openclaw/plugin-sdk/string-coerce-runtime"; |
10 | 10 | import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js"; |
| 11 | +import { redactSensitiveText } from "../logging/redact.js"; |
11 | 12 | import { createSubsystemLogger } from "../logging/subsystem.js"; |
12 | 13 | import { asRecord } from "../record-shared.js"; |
| 14 | +import { redactCdpUrl } from "./cdp.helpers.js"; |
13 | 15 | import type { ChromeMcpSnapshotNode } from "./chrome-mcp.snapshot.js"; |
14 | 16 | import type { BrowserTab } from "./client.types.js"; |
15 | 17 | import { BrowserProfileUnavailableError, BrowserTabNotFoundError } from "./errors.js"; |
@@ -432,11 +434,11 @@ async function createRealSession(
|
432 | 434 | const stderr = getStderr(); |
433 | 435 | if (stderr) { |
434 | 436 | log.warn( |
435 | | -`Chrome MCP attach failed for profile "${profileName}". Subprocess stderr:\n${stderr}`, |
| 437 | +`Chrome MCP attach failed for profile "${profileName}". Subprocess stderr:\n${redactSensitiveText(stderr)}`, |
436 | 438 | ); |
437 | 439 | } |
438 | 440 | const targetLabel = options.browserUrl |
439 | | - ? `the configured Chrome endpoint (${options.browserUrl})` |
| 441 | + ? `the configured Chrome endpoint (${redactCdpUrl(options.browserUrl) ?? options.browserUrl})` |
440 | 442 | : options.userDataDir |
441 | 443 | ? `the configured Chromium user data dir (${options.userDataDir})` |
442 | 444 | : "Google Chrome's default profile"; |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1027,10 +1027,11 @@ describe("chrome.ts internal", () => {
|
1027 | 1027 | return false; |
1028 | 1028 | }); |
1029 | 1029 | const fakeProc = makeFakeProc(); |
| 1030 | +const secretToken = "chrome-stderr-secret-1234567890"; // pragma: allowlist secret |
1030 | 1031 | spawnMock.mockImplementation(() => { |
1031 | 1032 | // Synthesize stderr data shortly after spawn. |
1032 | 1033 | void Promise.resolve().then(() => |
1033 | | -fakeProc.stderr.emit("data", Buffer.from("chrome crash log\n")), |
| 1034 | +fakeProc.stderr.emit("data", Buffer.from(`chrome crash log token=${secretToken}\n`)), |
1034 | 1035 | ); |
1035 | 1036 | return fakeProc; |
1036 | 1037 | }); |
@@ -1048,7 +1049,15 @@ describe("chrome.ts internal", () => {
|
1048 | 1049 | noSandbox: true, |
1049 | 1050 | extraArgs: [], |
1050 | 1051 | } as unknown as ResolvedBrowserConfig; |
1051 | | -await expect(launchOpenClawChrome(resolved, profile)).rejects.toThrow(/Chrome stderr:/); |
| 1052 | +let message = ""; |
| 1053 | +try { |
| 1054 | +await launchOpenClawChrome(resolved, profile); |
| 1055 | +} catch (err) { |
| 1056 | +message = err instanceof Error ? err.message : String(err); |
| 1057 | +} |
| 1058 | +expect(message).toContain("Chrome stderr:"); |
| 1059 | +expect(message).toContain("chrome crash log"); |
| 1060 | +expect(message).not.toContain(secretToken); |
1052 | 1061 | }); |
1053 | 1062 | |
1054 | 1063 | it("omits the sandbox hint on non-linux platforms", async () => { |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -7,6 +7,7 @@ import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runti
|
7 | 7 | import type { SsrFPolicy } from "../infra/net/ssrf.js"; |
8 | 8 | import { ensurePortAvailable } from "../infra/ports.js"; |
9 | 9 | import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js"; |
| 10 | +import { redactSensitiveText } from "../logging/redact.js"; |
10 | 11 | import { createSubsystemLogger } from "../logging/subsystem.js"; |
11 | 12 | import { CONFIG_DIR } from "../utils.js"; |
12 | 13 | import { hasChromeProxyControlArg, omitChromeProxyEnv } from "./browser-proxy-mode.js"; |
@@ -541,8 +542,9 @@ export async function launchOpenClawChrome(
|
541 | 542 | const diagnosticText = await diagnoseChromeCdp(profile.cdpUrl) |
542 | 543 | .then(formatChromeCdpDiagnostic) |
543 | 544 | .catch((err) => `CDP diagnostic failed: ${safeChromeCdpErrorMessage(err)}.`); |
544 | | -const stderrOutput = |
545 | | -normalizeOptionalString(Buffer.concat(stderrChunks).toString("utf8")) ?? ""; |
| 545 | +const stderrOutput = redactSensitiveText( |
| 546 | +normalizeOptionalString(Buffer.concat(stderrChunks).toString("utf8")) ?? "", |
| 547 | +); |
546 | 548 | if ( |
547 | 549 | allowSingletonRecovery && |
548 | 550 | CHROME_SINGLETON_IN_USE_PATTERN.test(stderrOutput) && |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。