fix(security): backport upstream v1.18 hardening for debug config, browser URLs and OAuth pages
AN
17h ago+802+802+012345678901234567890123456789-42-42-01234567890123456789
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.
debug configno longer prints secrets (starts from upstream82d4c89031, 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 likemax_tokensorauthorare kept - every header value
- URLs of any scheme with userinfo (e.g.
postgresql://user:pass@host/dbin 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/envmaps: every value (keys stay visible).command/headersCommand/argsargv (includingheadersCommandrecords 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 aDATABASE_URLwith a password still printed. We fix that here rather than waiting for upstream.
- values (including whole nested subtrees) whose key looks like a credential (api/access/private keys, secret, password/passwd/passphrase/
- 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 nothttp:/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.redirectToAuthorizationalso 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 toopen/xdg-open/rundll32 FileProtocolHandlerunchecked. Non-http(s) URLs are now refused, and the sign-in notice still shows the URL for manual copy.
- New
- Snowflake OAuth error page escapes provider text (the escaping part of upstream
e8fea9e63a). Our other callback pages already usedescapeHtml; 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 afile://or custom-scheme authorization URL is refused.packages/core:bun test test/open.test.ts→ 3 pass.- Every test file that mocks the
openmodule, plustest/cli,test/server/httpapi-*,test/altimate/learnand 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/opencodetest/altimate/debug→ pass.- Typecheck: all 13 workspace packages pass (
bun turbo typecheck, pre-push hook).bun.lockgains theopenentry for@opencode-ai/core. - Marker Guard, run with the fixed baseline-pinned analyzer from #1505 (the current one on
maincan'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.