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
- 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).
- Victim switches model via the picker UI with "Auto-restart Codex" checked.
- 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.
Summary
subprocess.Popen(["Codex.exe"], shell=True)is used as a fallback whenLOCALAPPDATAresolution fails on Windows. Usingshell=Truewith 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():When
LOCALAPPDATAis empty (e.g., unset by a process manager, a container, or a deliberately crafted environment),codex_exe.exists()returnsFalse, and the fallbackPopen(["Codex.exe"], shell=True)resolvesCodex.exevia thePATH. On Windows,shell=Trueinvokescmd.exe /C Codex.exe, which searchesPATHin order — any directory earlier inPATHthat contains a file namedCodex.exewill be executed instead of the real Codex.Why this matters
The
/api/switchendpoint (authenticated with a picker token) accepts a"restart_codex": truefield 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 namedCodex.exethe 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
Codex.exein a directory that appears early in the victim's%PATH%(e.g., a writablenode_modules/.binor a localscripts/folder)._restart_codex_app(). IfLOCALAPPDATAis empty or the resolved path does not exist, the fallbackPopen(["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=Truewith 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=Truefallback entirely and fail explicitly:If
LOCALAPPDATAis unset, log the condition rather than falling back to a PATH search.Acceptance criteria
shell=Trueremains in_restart_codex_appor_do_restart.codex_exedoes not exist, the function logs a warning and returns without spawning a subprocess.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=Truewith unqualified name is present in the source.