Summary
/api/models is an unauthenticated endpoint that enumerates every configured model slug, display name, and provider — including which model is currently active — to any caller that can reach the shim's loopback port. The route has no token check of any kind.
Evidence
codex_shim/server.py, ShimServer.app():
app.router.add_get("/api/models", self.api_models)
ShimServer.api_models() (no auth guard):
async def api_models(self, _request: web.Request) -> web.Response:
current = _current_managed_model()
data: list[dict[str, Any]] = []
...
return web.json_response(data)
Compare to switch_model, which does check the picker token:
async def switch_model(self, request: web.Request) -> web.Response:
if not self._valid_picker_token(request):
return web.json_response({"error": "forbidden"}, status=403)
The picker HTML at /picker embeds the token only for the switch call — the model list fetch uses bare fetch('/api/models') with no Authorization header.
Why this matters
The endpoint reveals the full set of configured upstream providers (Anthropic, OpenAI, Ollama, Cursor, ChatGPT passthrough), their display names, and which model is active. An attacker with DNS rebinding access can silently inventory the target's AI toolchain. The same information can be useful to social-engineering attacks or to a malicious browser extension running on the user's machine.
Attack or failure scenario
- Attacker hosts
evil.com resolving to 127.0.0.1 via DNS rebinding.
- Victim visits
evil.com; their browser sends GET http://127.0.0.1:8765/api/models — the Host header is evil.com, which the host_guard_middleware rejects for normal loopback variants.
- However: if the victim has set
CODEX_SHIM_ALLOWED_HOSTS=shim.local and the attacker can predict or guess that value, or if the user has --host 0.0.0.0 (which disables the wildcard guard), /api/models is fully open. More practically, a malicious browser extension or local script can hit 127.0.0.1:8765/api/models directly, bypassing the Host header guard entirely, because extensions are not subject to Host-header origin restrictions.
Root cause
The /api/models route was designed as a companion to the picker UI. The picker page itself requires the browser to already hold the picker token (embedded in the page HTML), but the data endpoint that populates the picker was not guarded with the same token check.
Recommended fix
Apply the same _valid_picker_token check to api_models:
async def api_models(self, request: web.Request) -> web.Response:
if not self._valid_picker_token(request):
return web.json_response({"error": "forbidden"}, status=403)
...
This is consistent with the existing token design and requires no architectural change.
Acceptance criteria
GET /api/models without the X-Codex-Shim-Picker-Token header returns HTTP 403.
GET /api/models with the correct token continues to return model list.
- Existing test
test_api_models_lists_configured_models_with_active_flag is updated to pass the picker token, and a new test verifies the 403 path.
Suggested labels
security, bug
Priority
P2
Severity
Medium — information disclosure only; the host-guard provides a meaningful partial barrier, but it is not an absolute defence against local extensions or misconfigured bind hosts.
Confidence
Confirmed — the absence of an auth check on api_models is unambiguous in the source.
Summary
/api/modelsis an unauthenticated endpoint that enumerates every configured model slug, display name, and provider — including which model is currently active — to any caller that can reach the shim's loopback port. The route has no token check of any kind.Evidence
codex_shim/server.py,ShimServer.app():ShimServer.api_models()(no auth guard):Compare to
switch_model, which does check the picker token:The picker HTML at
/pickerembeds the token only for theswitchcall — the model list fetch uses barefetch('/api/models')with noAuthorizationheader.Why this matters
The endpoint reveals the full set of configured upstream providers (Anthropic, OpenAI, Ollama, Cursor, ChatGPT passthrough), their display names, and which model is active. An attacker with DNS rebinding access can silently inventory the target's AI toolchain. The same information can be useful to social-engineering attacks or to a malicious browser extension running on the user's machine.
Attack or failure scenario
evil.comresolving to127.0.0.1via DNS rebinding.evil.com; their browser sendsGET http://127.0.0.1:8765/api/models— the Host header isevil.com, which thehost_guard_middlewarerejects for normal loopback variants.CODEX_SHIM_ALLOWED_HOSTS=shim.localand the attacker can predict or guess that value, or if the user has--host 0.0.0.0(which disables the wildcard guard),/api/modelsis fully open. More practically, a malicious browser extension or local script can hit127.0.0.1:8765/api/modelsdirectly, bypassing the Host header guard entirely, because extensions are not subject to Host-header origin restrictions.Root cause
The /api/models route was designed as a companion to the picker UI. The picker page itself requires the browser to already hold the picker token (embedded in the page HTML), but the data endpoint that populates the picker was not guarded with the same token check.
Recommended fix
Apply the same
_valid_picker_tokencheck toapi_models:This is consistent with the existing token design and requires no architectural change.
Acceptance criteria
GET /api/modelswithout theX-Codex-Shim-Picker-Tokenheader returns HTTP 403.GET /api/modelswith the correct token continues to return model list.test_api_models_lists_configured_models_with_active_flagis updated to pass the picker token, and a new test verifies the 403 path.Suggested labels
security, bug
Priority
P2
Severity
Medium — information disclosure only; the host-guard provides a meaningful partial barrier, but it is not an absolute defence against local extensions or misconfigured bind hosts.
Confidence
Confirmed — the absence of an auth check on
api_modelsis unambiguous in the source.