Skip to content

Proxy forwards auth headers to arbitrary upstream URLs, enabling SSRF and credential theft via malicious config routes #22

Description

@tg12

Summary

subprocess.Popen(["Codex.exe"], shell=True) is used as a fallback when LOCALAPPDATA resolution fails on Windows. Using shell=True with a bare executable name performs a shell-expanded PATH search, which is exploitable via PATH hijacking.

Evidence

codex_shim/server.py, _restart_codex_app() / _do_restart():

local_appdata = _os.environ.get("LOCALAPPDATA", "")
codex_exe = Path(local_appdata) / "Programs" / "Codex" / "Codex.exe"
if codex_exe.exists():
    _subprocess.Popen([str(codex_exe)])
else:
    _subprocess.Popen(["Codex.exe"], shell=True)

When LOCALAPPDATA is empty (e.g., unset by a process manager, a container, or a deliberately crafted environment), codex_exe.exists() returns False, and the fallback Popen(["Codex.exe"], shell=True) resolves Codex.exe via the PATH. On Windows, shell=True invokes cmd.exe /C Codex.exe, which searches PATH in order — any directory earlier in PATH that contains a file named Codex.exe will be executed instead of the real Codex.

Why this matters

The /api/switch endpoint (authenticated with a picker token) accepts a "restart_codex": true field and calls _restart_codex_app(). If an attacker has already compromised PATH ordering (common in shared user environments, or via a malicious npm package), they can get the shim to execute an arbitrary binary named Codex.exe the next time any authenticated picker user switches models with restart enabled. The triggered binary runs in the context of the user who started the shim.

Attack or failure scenario

  1. Attacker plants a trojanized Codex.exe in a directory that appears early in the victim's %PATH% (e.g., a writable node_modules/.bin or a local scripts/ folder).
  2. Victim switches model via the picker UI with "Auto-restart Codex" checked.
  3. Shim calls _restart_codex_app(). If LOCALAPPDATA is empty or the resolved path does not exist, the fallback Popen(["Codex.exe"], shell=True) executes the malicious binary.

This is a persistence/escalation step in a broader attack; it requires a separate PATH-hijack primitive but is entirely realistic in developer environments with many npm/pip tools on PATH.

Root cause

shell=True with an unqualified executable name is a well-known Windows PATH hijacking vector. The intent was a graceful fallback, but it trades correctness for safety. If the known path doesn't exist, the right failure mode is logging an error and returning, not executing an unresolved binary.

Recommended fix

Remove the shell=True fallback entirely and fail explicitly:

if codex_exe.exists():
    _subprocess.Popen([str(codex_exe)])
else:
    # Cannot locate Codex.exe; skip restart silently rather than PATH-search
    import sys as _sys
    print("[restart] LOCALAPPDATA not set or Codex.exe not found; skipping restart", file=_sys.stderr)
    return

If LOCALAPPDATA is unset, log the condition rather than falling back to a PATH search.

Acceptance criteria

  • No shell=True remains in _restart_codex_app or _do_restart.
  • If codex_exe does not exist, the function logs a warning and returns without spawning a subprocess.
  • A test verifies that when the resolved path does not exist, no subprocess is spawned.

Suggested labels

security, bug

Priority

P2

Severity

Medium — requires a prior PATH-hijack primitive; not directly exploitable remotely, but the fallback is unambiguously unsafe and should not exist.

Confidence

Confirmed — shell=True with unqualified name is present in the source.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions