Skip to content

_dump_debug_request unconditionally writes full conversation body to a world-readable file in the project directory #46

Description

@tg12

Summary

_dump_debug_request writes the full forwarded request body — including the Authorization header value (which contains BYOK API keys) — to .codex-shim/last_request.json on disk. This file is world-readable by default on most Unix systems and is stored in the project checkout directory (a version-controlled location on many developer machines).

Evidence

codex_shim/server.py, _dump_debug_request():

def _dump_debug_request(slug: str, url: str, body: dict[str, Any]) -> None:
    try:
        dump_path = DEBUG_DIR / "last_request.json"
        dump_path.parent.mkdir(parents=True, exist_ok=True)
        payload = {"slug": slug, "url": url, "body": body}
        full = json.dumps(payload, indent=2, default=str)
        ...
        dump_path.write_text(full)

DEBUG_DIR is defined as:

DEBUG_DIR = Path(__file__).resolve().parents[1] / ".codex-shim"

This resolves to the project root's .codex-shim/ directory — directly inside the repository checkout. .gitignore lists .codex-shim/ so the file is not committed, but:

  1. The directory is readable by any user with access to the project directory (no mode=0o700 passed to mkdir).
  2. dump_path.write_text(full) uses the default umask — typically 0o644, making the file readable by group and world.
  3. The body dict may include messages with the user's code context, tool outputs, and conversation history — sensitive work product beyond just API keys.

The function is called unconditionally on every forwarded _post_openai_chat and _post_openai_chat_as_anthropic request (not behind a debug flag), so this file is always written in production use.

Note: The HTTP headers (containing Authorization: Bearer <api_key>) are not included in the dumped payload (only slug, url, body are serialised), but the request body can contain conversation history and code context that should be considered private.

Why this matters

  1. Any local user with read access to the project directory can read the last conversation sent through the shim, including potentially sensitive code, credentials embedded in prompts, or security-related context.
  2. On CI/CD systems or shared developer machines, this file may be accessible to other users or processes.
  3. If a user accidentally runs git add .codex-shim/ (the gitignore protects against git add . but not explicit adds), conversation content could be committed to a repository.

Root cause

The debug dump was added as a development aid but was never gated behind a debug flag. It runs unconditionally in production.

Recommended fix

  1. Gate the dump behind an environment variable: if os.environ.get("CODEX_SHIM_DEBUG_DUMP"):.
  2. Create the .codex-shim/ directory with mode 0o700: dump_path.parent.mkdir(parents=True, exist_ok=True, mode=0o700).
  3. Write the dump file with mode 0o600: use dump_path.open("w") with os.umask set, or dump_path.write_text(...); dump_path.chmod(0o600).

Acceptance criteria

  • _dump_debug_request is a no-op unless CODEX_SHIM_DEBUG_DUMP=1 is set.
  • When the dump is enabled, the file is created with mode 0o600.
  • The README documents the CODEX_SHIM_DEBUG_DUMP flag as a debugging tool.

Suggested labels

security, privacy, bug

Priority

P2

Severity

Medium — local information disclosure of conversation content; API keys are not included in the dump payload.

Confidence

Confirmed — unconditional write with no permission hardening is explicit in the source.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions