Skip to content

fix(config): redact browser CDP URLs in config snapshots#53418

Closed
coygeek wants to merge 2 commits intoopenclaw:mainfrom
coygeek:codex/de-dny-remote-cdp-url-redaction-bypass
Closed

fix(config): redact browser CDP URLs in config snapshots#53418
coygeek wants to merge 2 commits intoopenclaw:mainfrom
coygeek:codex/de-dny-remote-cdp-url-redaction-bypass

Conversation

@coygeek
Copy link
Copy Markdown
Contributor

@coygeek coygeek commented Mar 24, 2026

Fix Summary

Authenticated clients with only operator.read scope can call config.get and receive the full, unredacted browser.cdpUrl and browser.profiles.*.cdpUrl values, including embedded query tokens (e.g., Browserless ?token=...) and HTTP Basic credentials (e.g., user:pass@host). The leaked credentials are reusable outside OpenClaw to connect directly to the upstream browser service, bypassing OpenClaw's audit trail and scope checks.

Issue Linkage

Fixes #53433

Security Snapshot

  • CVSS v3.1: 9.9 (Critical)
  • CVSS v4.0: 9.4 (Critical)

Implementation Details

Files Changed

  • docs/.generated/config-baseline.json (+5/-3)
  • docs/.generated/config-baseline.jsonl (+2/-2)
  • src/config/redact-snapshot.schema.test.ts (+26/-0)
  • src/config/redact-snapshot.test-hints.ts (+2/-0)
  • src/config/redact-snapshot.test.ts (+55/-0)
  • src/config/redact-snapshot.ts (+1/-1)
  • src/config/schema.base.generated.ts (+4/-2)
  • src/config/zod-schema.ts (+2/-2)
  • ui/src/ui/chat/slash-command-executor.ts (+7/-3)

Technical Analysis

  1. Configure a remote browser profile with an auth-bearing CDP URL:
    { browser: { profiles: { remote: { cdpUrl: "https://alice:secret@example.com/chrome?token=supersecret123" } } } }
  2. Connect a Gateway client that has only operator.read scope.
  3. Call config.get.
  4. Observe the response contains the full unredacted cdpUrl in payload.config.browser.profiles.remote.cdpUrl, including the query token and Basic-auth userinfo.

Validation Evidence

  • Command: pnpm exec oxlint src/ && pnpm build && pnpm check && pnpm test
  • Status: passed_with_baseline_failures

Risk and Compatibility

non-breaking; no known regression impact

AI-Assisted Disclosure

  • AI-assisted: yes
  • Model: opencode/github-copilot/gpt-5.4

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 24, 2026

Greptile Summary

This PR fixes a credential-leak vulnerability where browser.cdpUrl and browser.profiles.*.cdpUrl values (including embedded query tokens and HTTP Basic credentials) were exposed in config.get responses. The fix is layered correctly: sensitive: true is added to both schema hint paths in schema.base.generated.ts and zod-schema.ts, .cdpUrl is added to the isUserInfoUrlPath() fallback in redact-snapshot.ts, and tests cover both the hint-based and no-hint redaction paths.

  • The state.chatModelCatalog reference on line 165 of slash-command-executor.ts is a dangling undefined that will throw ReferenceError whenever /model <value> is used from the Web UI, silently falling back to the catch block with a failure message and no sessionPatch returned.

Confidence Score: 4/5

The security fix is correct and well-tested, but a P1 runtime bug in the unrelated UI change breaks the /model slash command in the Web UI.

The core security fix (schema hints, zod registration, fallback path, tests) is solid and complete. However, this PR also modifies slash-command-executor.ts and introduces a reference to an undefined state variable that causes a ReferenceError whenever /model value is used — that's a P1 bug on the changed path that should be resolved before merging.

ui/src/ui/chat/slash-command-executor.ts — line 165 references undefined state

Prompt To Fix All With AI
This is a comment left during a code review.
Path: ui/src/ui/chat/slash-command-executor.ts
Line: 165

Comment:
**`state` is not defined in this scope**

`state` is referenced at line 165 but is not a parameter to `executeSlashCommand` or `executeModel`, and is not imported or declared anywhere in this module. At runtime, this throws `ReferenceError: state is not defined`, which is caught by the surrounding try/catch and returned as `"Failed to set model: ReferenceError: state is not defined"` — so `/model <value>` silently misfires and the `sessionPatch.modelOverride` in the response is never set.

The caller in `app-chat.ts` is `executeSlashCommand(host.client, targetSessionKey, name, args)` and does not pass catalog state. The function signature needs a `catalog` (or `state`) parameter, or the catalog should be fetched inside `executeModel` (which already fetches session and model data via the client).

```suggestion
      ? normalizeChatModelOverrideValue(rawOverride, []) ||
```

(Temporary safe fallback — the proper fix is to either pass the catalog as a parameter to `executeSlashCommand`/`executeModel`, or fetch it from `models.list` which is already called in the `!args` branch.)

How can I resolve this? If you propose a fix, please make it concise.

Reviews (3): Last reviewed commit: "fix(de-dny-remote-cdp-url-redaction-bypa..." | Re-trigger Greptile

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b0d95821b6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/config/zod-schema.ts
enabled: z.boolean().optional(),
evaluateEnabled: z.boolean().optional(),
cdpUrl: z.string().optional(),
cdpUrl: z.string().optional().register(sensitive),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Regenerate shipped schema hints for cdpUrl redaction

Marking cdpUrl as sensitive in OpenClawSchema is not sufficient by itself because config.get redaction uses schema.uiHints from GENERATED_BASE_CONFIG_SCHEMA (src/config/schema.ts), and this commit does not update src/config/schema.base.generated.ts where browser.cdpUrl / browser.profiles.*.cdpUrl still lack sensitive: true. In that state, CDP URLs that carry only query credentials (for example ?token=... without user:pass@) are still returned unredacted in config snapshots, so the security fix remains incomplete until the generated schema payload is regenerated and committed.

Useful? React with 👍 / 👎.

Comment on lines +78 to +102
it("redacts browser.cdpUrl and browser.profiles.*.cdpUrl via schema hints", () => {
const snapshot = makeSnapshot({
browser: {
cdpUrl: "wss://chrome.browserless.io/?token=secret-token-123",
profiles: {
remote: {
cdpUrl: "https://user:pass@browserbase.example.test/connect",
cdpPort: 9222,
},
},
},
});

const result = redactConfigSnapshot(snapshot, mainSchemaHints);
const config = result.config as typeof snapshot.config;
expect(config.browser.cdpUrl).toBe(REDACTED_SENTINEL);
expect(config.browser.profiles.remote.cdpUrl).toBe(REDACTED_SENTINEL);
expect(config.browser.profiles.remote.cdpPort).toBe(9222);

const restored = restoreRedactedValues(result.config, snapshot.config, mainSchemaHints);
expect(restored.browser.cdpUrl).toBe("wss://chrome.browserless.io/?token=secret-token-123");
expect(restored.browser.profiles.remote.cdpUrl).toBe(
"https://user:pass@browserbase.example.test/connect",
);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Schema test validates fixture, not the real generated hints

This test imports redactSnapshotTestHints (from redact-snapshot.test-hints.ts) aliased as mainSchemaHints. The test fixture was updated to include "browser.cdpUrl": { sensitive: true }, so the test passes. However, the actual production-facing hints file (src/config/schema.base.generated.ts) was NOT updated — it still lacks sensitive: true for both "browser.cdpUrl" and "browser.profiles.*.cdpUrl":

// Current state of schema.base.generated.ts (unchanged by this PR):
"browser.cdpUrl": {
  label: "Browser CDP URL",
  tags: ["advanced"],
  // sensitive: true  <-- MISSING
},
"browser.profiles.*.cdpUrl": {
  label: "Browser Profile CDP URL",
  tags: ["storage"],
  // sensitive: true  <-- MISSING
},

The PR description explicitly names regenerating this file as one of three fix layers, but the generator (scripts/generate-base-config-schema.ts) was not re-run after adding .register(sensitive) to zod-schema.ts.

Concrete exposure: a CDP URL that uses only a query-string token with no embedded user:pass@ credentials would pass through unredacted when real schema hints (from GENERATED_BASE_CONFIG_SCHEMA) are used. stripUrlUserInfo only strips userinfo credentials; query-string tokens are left intact. The test only passes because the fixture has sensitive: true, masking this gap.

The fix is to re-run the schema generator so schema.base.generated.ts includes sensitive: true for both cdpUrl hint entries.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/config/redact-snapshot.schema.test.ts
Line: 78-102

Comment:
**Schema test validates fixture, not the real generated hints**

This test imports `redactSnapshotTestHints` (from `redact-snapshot.test-hints.ts`) aliased as `mainSchemaHints`. The test fixture was updated to include `"browser.cdpUrl": { sensitive: true }`, so the test passes. However, the actual production-facing hints file (`src/config/schema.base.generated.ts`) was **NOT updated** — it still lacks `sensitive: true` for both `"browser.cdpUrl"` and `"browser.profiles.*.cdpUrl"`:

```ts
// Current state of schema.base.generated.ts (unchanged by this PR):
"browser.cdpUrl": {
  label: "Browser CDP URL",
  tags: ["advanced"],
  // sensitive: true  <-- MISSING
},
"browser.profiles.*.cdpUrl": {
  label: "Browser Profile CDP URL",
  tags: ["storage"],
  // sensitive: true  <-- MISSING
},
```

The PR description explicitly names regenerating this file as one of three fix layers, but the generator (`scripts/generate-base-config-schema.ts`) was not re-run after adding `.register(sensitive)` to `zod-schema.ts`.

**Concrete exposure**: a CDP URL that uses only a query-string token with no embedded `user:pass@` credentials would pass through unredacted when real schema hints (from `GENERATED_BASE_CONFIG_SCHEMA`) are used. `stripUrlUserInfo` only strips userinfo credentials; query-string tokens are left intact. The test only passes because the fixture has `sensitive: true`, masking this gap.

The fix is to re-run the schema generator so `schema.base.generated.ts` includes `sensitive: true` for both `cdpUrl` hint entries.

How can I resolve this? If you propose a fix, please make it concise.

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation app: web-ui App: web-ui labels Mar 24, 2026
@coygeek
Copy link
Copy Markdown
Contributor Author

coygeek commented Mar 24, 2026

Re: Greptile review — schema.base.generated.ts regeneration

Verified: running pnpm exec tsx scripts/generate-base-config-schema.ts after the .register(sensitive) changes to zod-schema.ts produces byte-identical output to what's in this diff. The generator correctly propagates sensitive: true and the security tag to both browser.cdpUrl and browser.profiles.*.cdpUrl entries.

The test fixture approach (redactSnapshotTestHints) is the existing pattern used by all other tests in redact-snapshot.schema.test.ts — the fixture mirrors the generated hints and the schema-driven test validates the full round-trip (redact → restore) against it.

No code changes needed; the diff is already correct via the proper codegen path.

Copy link
Copy Markdown
Contributor

@fabianwilliams fabianwilliams left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid security fix. CDP URLs can contain embedded credentials (tokens, userinfo) and should absolutely be treated as sensitive.

What's done right:

  • Both browser.cdpUrl and browser.profiles.*.cdpUrl marked sensitive: true in schema + config baseline
  • isUserInfoUrlPath extended to catch .cdpUrl endings — catches credential-in-URL patterns
  • Zod schema registers both paths with the sensitive marker
  • Comprehensive tests: embedded credentials in both global and per-profile cdpUrl, plus raw config string redaction
  • Restore round-trip verified (redact → restore returns original values)
  • Non-sensitive fields like cdpPort confirmed unaffected

The UI import reformatting is incidental but clean. LGTM — approve.

@coygeek
Copy link
Copy Markdown
Contributor Author

coygeek commented Apr 13, 2026

@greptile-apps review

const resolvedValue = rawOverride
? (normalizeChatModelOverrideValue(rawOverride, state.chatModelCatalog ?? []) ||
resolveServerChatModelValue(patchedModel, patched.resolved?.modelProvider))
? normalizeChatModelOverrideValue(rawOverride, state.chatModelCatalog ?? []) ||
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 state is not defined in this scope

state is referenced at line 165 but is not a parameter to executeSlashCommand or executeModel, and is not imported or declared anywhere in this module. At runtime, this throws ReferenceError: state is not defined, which is caught by the surrounding try/catch and returned as "Failed to set model: ReferenceError: state is not defined" — so /model <value> silently misfires and the sessionPatch.modelOverride in the response is never set.

The caller in app-chat.ts is executeSlashCommand(host.client, targetSessionKey, name, args) and does not pass catalog state. The function signature needs a catalog (or state) parameter, or the catalog should be fetched inside executeModel (which already fetches session and model data via the client).

Suggested change
? normalizeChatModelOverrideValue(rawOverride, state.chatModelCatalog ?? []) ||
? normalizeChatModelOverrideValue(rawOverride, []) ||

(Temporary safe fallback — the proper fix is to either pass the catalog as a parameter to executeSlashCommand/executeModel, or fetch it from models.list which is already called in the !args branch.)

Prompt To Fix With AI
This is a comment left during a code review.
Path: ui/src/ui/chat/slash-command-executor.ts
Line: 165

Comment:
**`state` is not defined in this scope**

`state` is referenced at line 165 but is not a parameter to `executeSlashCommand` or `executeModel`, and is not imported or declared anywhere in this module. At runtime, this throws `ReferenceError: state is not defined`, which is caught by the surrounding try/catch and returned as `"Failed to set model: ReferenceError: state is not defined"` — so `/model <value>` silently misfires and the `sessionPatch.modelOverride` in the response is never set.

The caller in `app-chat.ts` is `executeSlashCommand(host.client, targetSessionKey, name, args)` and does not pass catalog state. The function signature needs a `catalog` (or `state`) parameter, or the catalog should be fetched inside `executeModel` (which already fetches session and model data via the client).

```suggestion
      ? normalizeChatModelOverrideValue(rawOverride, []) ||
```

(Temporary safe fallback — the proper fix is to either pass the catalog as a parameter to `executeSlashCommand`/`executeModel`, or fetch it from `models.list` which is already called in the `!args` branch.)

How can I resolve this? If you propose a fix, please make it concise.

@coygeek
Copy link
Copy Markdown
Contributor Author

coygeek commented Apr 22, 2026

Closing this as superseded by #67679, which has already merged and carries the CDP URL redaction fix on current main. I re-verified the issue against a fresh clone at commit e39784d, so this older PR is no longer needed.

@coygeek coygeek closed this Apr 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: web-ui App: web-ui docs Improvements or additions to documentation size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: remote CDP URLs bypass config redaction and leak credentials

2 participants