WebSocket connect logs print the full base URL, leaking credentials embedded in openai_base_url

Open 💬 1 comment Opened Aug 11, 2026 by LeiGaoRobot

Summary

The Responses WebSocket transport logs the full connection URL on connect, success, and failure. When openai_base_url (or a model_providers.*.base_url) carries a credential — a capability-URL path segment, or URL userinfo — that secret ends up verbatim in the CLI's stderr / log output.

Local proxies and gateways in front of Codex commonly authenticate exactly this way, because the built-in openai provider's root openai_base_url override cannot carry a custom header, so the URL itself is the only place a local admission token can live (e.g. http://127.0.0.1:10100/t/<token>/v1).

Where

codex-rs/codex-api/src/endpoint/responses_websocket.rs (connect_websocket, as of 3d4d253f8f4a):

The error! site is the worst of the three: it fires at default log level on every WS fallback (e.g. a gateway answering 426 Upgrade Required), so the secret lands in plain stderr during otherwise-normal operation, not just under RUST_LOG debugging.

Reproduction

  1. Point Codex at any local endpoint whose URL embeds a secret, e.g. in ~/.codex/config.toml:

``toml
openai_base_url = "http://127.0.0.1:10100/t/SECRET-TOKEN/v1"
``

  1. Have the endpoint reject the WebSocket upgrade (HTTP 426) so Codex falls back to HTTP.
  2. Run codex exec "hi". Observed on codex-cli 0.147.0:

``
ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 426 Upgrade Required, url: ws://127.0.0.1:10100/t/SECRET-TOKEN/v1/responses
``

The HTTP fallback then works fine — which makes it easy to copy this stderr into bug reports, CI logs, or shared terminals without noticing the token.

Suggested fix

Redact the URL before logging at these sites: strip userinfo and query, and mask path segments (or at least log only scheme://host:port). Something like a small redacted_url(&Url) -> String helper used by all three call sites; map_ws_error may want the same treatment if the URL is embedded into the returned error string.

Happy to send a PR if that shape sounds right.

View original on GitHub ↗

1 Comment

jdcodes1 · 9 days ago

Verified all three sites still on main (1f41cc5d92): responses_websocket.rs L494 (info!), L509 (info! + response headers), L515 (error!), and confirmed the error! fires at default level on every WS→HTTPS fallback.

What makes this an inconsistency rather than just an oversight: the codebase already has a redaction convention for exactly this class of value — HttpClient::new_without_request_logging exists "for endpoints whose URLs or headers may contain credentials" (https://github.com/openai/codex/blob/1f41cc5d92/codex-rs/http-client/src/client.rs#L35-L41), and the outbound-proxy structs deliberately print url: <redacted> in Debug output for the same reason (http-client/src/outbound_proxy.rs#L149-L155). The WebSocket transport just never adopted the convention.

Minimal fix: a sanitized_ws_url() that strips URL userinfo and replaces path segments beyond the provider root with (host+port are what's diagnostically useful), applied at all three call sites; the success-path header dump at L509 should also drop or redact set-cookie-class headers. That keeps the connect/fallback diagnostics (which are genuinely useful — see the TUN-proxy debugging in #38402) without ever writing the capability token to stderr.