[Security] High: Responses-API proxy forwards unauthenticated browser/local requests with operator bearer token

Resolved 💬 2 comments Opened Apr 13, 2026 by parasol-aser Closed Apr 13, 2026

Severity

High — Confirmed (working PoC against default config)

CWE / References

  • CWE-352 (Cross-Site Request Forgery)
  • CWE-306 (Missing Authentication for Critical Function)
  • OWASP Top 10 A01:2021 — Broken Access Control

Affected file

codex-rs/responses-api-proxy/src/lib.rs — functions bind_listener(), forward_request(), write_server_info() (entry point around codex-rs/responses-api-proxy/src/lib.rs:138).

Audited commit: 49ca7c9f24ede84ce50de837516070761385c1a9 (HEAD of main).

Root cause

The responses-api-proxy binary binds 127.0.0.1:<port> and accepts any POST /v1/responses. It strips the inbound Authorization and Host headers, forwards all other headers verbatim, and injects the operator's bearer token (read once from stdin at startup) before forwarding upstream.

There is:

  • no CSRF token,
  • no Origin / Referer validation,
  • no content-type gate that would force a CORS preflight,
  • no shared-secret or ACL for local clients.

The documented --server-info <file> option writes { "port": ..., "pid": ... } and makes port discovery trivial for any local process.

Impact

Two concrete attack paths share this root cause:

  1. Browser CSRF. Any web page the operator visits can issue a simple cross-origin POST that the browser will send with the attacker's prompt. The proxy attaches the operator's OpenAI bearer token and forwards it upstream. CORS only blocks the attacker from reading the response — the request still executes. Result: arbitrary billed LLM calls, quota burn, audit-log pollution, feature-flag triggers.
  2. Local co-tenant piggyback. Any local process that learns the loopback port (via server-info.json, ss/lsof, supervisor logs, etc.) can spend the operator's bearer token directly.

Reproduction

Browser-origin CSRF (confirmed end-to-end against a local proxy instance):

curl -i -s -X POST 'http://127.0.0.1:18080/v1/responses' \
  -H 'Origin: https://attacker.example' \
  -H 'Content-Type: text/plain;charset=UTF-8' \
  --data '{"model":"gpt-4o","input":"burn quota"}'

Browser equivalent (a page at https://attacker.example can run this):

fetch("http://127.0.0.1:18080/v1/responses", {
  method: "POST",
  headers: { "Content-Type": "text/plain;charset=UTF-8" },
  body: JSON.stringify({ model: "gpt-4o", input: "burn quota" }),
});

Local co-tenant piggyback:

PROXY_PORT=$(jq .port /tmp/server-info.json)
curl -s -X POST "http://127.0.0.1:${PROXY_PORT}/v1/responses" \
  -H 'Content-Type: application/json' \
  --data '{"model":"gpt-4o","input":"whoami"}'

Suggested fix

  1. Per-launch shared secret. Generate a random token at proxy startup, write it alongside the port in server-info.json (file mode 0600), and reject every request whose Authorization / X-Proxy-Token header does not match via constant-time compare.
  2. Harden the browser surface independently. Reject requests whose Origin header is set and not on an explicit allowlist, and require Content-Type: application/json so that browsers must preflight. Combine with (1) so CLI clients still work after presenting the shared secret.
  3. Tighten write_server_info(). Create the output file with mode 0600 and refuse world-readable target paths.

After the fix, add an integration test that asserts the proxy rejects unauthenticated requests by default, so regressions fail CI rather than ship.

---
Filed as part of a third-party security audit of openai/codex at commit 49ca7c9. This finding is the only item classified Confirmed at High severity; two additional Likely High-severity issues on exec-server and app-server WebSocket listeners are being tracked separately.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗