fix(security): backport upstream v1.18 hardening for debug config, browser URLs and OAuth pages

AN
17h ago
+802-42

Issue for this PR

Closes #1506

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Backports three hardening fixes from upstream v1.18 that we don't have, since we're based on v1.17.9. We are not doing another v1 merge, so we cherry-pick these instead.

  1. debug config no longer prints secrets (starts from upstream 82d4c89031, extended after review). Only the printed output is redacted; the resolved config used at runtime is unchanged. Masked:
    • values (including whole nested subtrees) whose key looks like a credential (api/access/private keys, secret, password/passwd/passphrase/*_pwd, token, authorization, bearer, *_auth, cookie, dsn, connection string), while keys like max_tokens or author are kept
    • every header value
    • URLs of any scheme with userinfo (e.g. postgresql://user:pass@host/db in MCP env, jdbc:...): whole value
    • every query and fragment parameter value in any URL (?key=***&sig=***); names, host and path stay readable. This is deliberately name-agnostic because credential query names are open-ended.
    • argv arrays: --token=… and the value after a bare secret flag
    • MCP environment/env maps: every value (keys stay visible). command/headersCommand/args argv (including headersCommand records of header → argv): everything except the executable and non-string values. These carriers are free-form, so they are masked structurally (JDBC property strings, env TOKEN=…, --header 'Authorization: …').
      Upstream's version only checked http(s) URLs and skipped array strings, so a DATABASE_URL with a password still printed. We fix that here rather than waiting for upstream.
  2. Only http(s) URLs are opened in the browser (upstream 29f07e0c69):
    • New openUrl() in @opencode-ai/core/open (same as upstream) rejects anything that is not http:/https:.
    • Every production open() call is switched to it: 14 sites across opencode, tui and our Altimate plugin/workspace code. Each keeps its existing error handling.
    • McpOAuthProvider.redirectToAuthorization also refuses non-http(s) authorization URLs.
    • Main risk closed: an MCP server could previously make us open file:// or custom-scheme URLs.
    • The refusal error names the MCP server by its config name, not its URL, because that message is copied into status telemetry and warnings and server URLs can carry credentials. This differs from upstream's wording.
    • Same check added in our own drivers/src/sign-in.ts. Snowflake's externalbrowser SSO URL comes from the server/IdP and was passed to open/xdg-open/rundll32 FileProtocolHandler unchecked. Non-http(s) URLs are now refused, and the sign-in notice still shows the URL for manual copy.
  3. Snowflake OAuth error page escapes provider text (the escaping part of upstream e8fea9e63a). Our other callback pages already used escapeHtml; this was the one that didn't.

All edits inside upstream files carry altimate_change … upstream_fix: backport <sha> markers so the next merge can drop them in favor of upstream's versions.

Behavior changes (was → now):

  • debug config: raw secrets → ***. MCP environment values and non-flag command arguments are now always shown as ***, so the output is less detailed for debugging.
  • Any attempt to open a non-http(s) URL (MCP OAuth, TUI links, workspace handoff, etc.): opened via the OS handler → rejected. Each call site's existing failure path runs instead (toast, printed URL, or silent catch). All URLs we construct ourselves are http(s), including the localhost trace viewer and web UI.
  • Snowflake OAuth error page: raw HTML → escaped text.

How did you verify your code works?

Unit/integration tests, run locally outside the sandbox:

  • packages/opencode: bun test test/cli/debug-config.test.ts test/mcp/oauth-browser.test.ts test/mcp/oauth-provider.test.ts test/plugin/snowflake-cortex.test.ts → pass (debug-config + oauth-provider + oauth-browser: 95 tests). The debug-config test spawns the real CLI with a config containing secrets and asserts they are masked. The oauth-browser tests check that an MCP server returning a file:// or custom-scheme authorization URL is refused.
  • packages/core: bun test test/open.test.ts → 3 pass.
  • Every test file that mocks the open module, plus test/cli, test/server/httpapi-*, test/altimate/learn and the workspace rebind test: 2988 pass, 0 fail.
  • packages/drivers: bun test → 382 pass, 0 fail (new spawn-stubbed tests: file:, javascript:, ms-msdt: and garbage are refused). packages/opencode test/altimate/debug → pass.
  • Typecheck: all 13 workspace packages pass (bun turbo typecheck, pre-push hook). bun.lock gains the open entry for @opencode-ai/core.
  • Marker Guard, run with the fixed baseline-pinned analyzer from #1505 (the current one on main can't see upstream files): 11 upstream-shared files checked, all custom code marked.

Not run: a manual OAuth flow against a real MCP server or Snowflake.

Screenshots / recordings

N/A

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Security
    • Browser launches now accept only valid HTTP(S) URLs, preventing unsafe schemes from being opened.
    • Debug configuration output masks sensitive values, including credentials, tokens, and secret command-line arguments.
    • OAuth error pages safely escape displayed error messages.