WebSocket connect logs print the full base URL, leaking credentials embedded in openai_base_url
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):
- https://github.com/openai/codex/blob/3d4d253f8f4a/codex-rs/codex-api/src/endpoint/responses_websocket.rs#L495 —
info!("connecting to websocket: {url}") info!("successfully connected to websocket: {url}, headers: {:?}", …)a few lines below- https://github.com/openai/codex/blob/3d4d253f8f4a/codex-rs/codex-api/src/endpoint/responses_websocket.rs#L516 —
error!("failed to connect to websocket: {err}, url: {url}")
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
- 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"
- Have the endpoint reject the WebSocket upgrade (HTTP 426) so Codex falls back to HTTP.
- 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.
1 Comment
Verified all three sites still on
main(1f41cc5d92):responses_websocket.rsL494 (info!), L509 (info!+ response headers), L515 (error!), and confirmed theerror!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_loggingexists "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 printurl: <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 redactset-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.