Skip to content

Bifrost CVE-2026-90898: unauthenticated RCE because a stdio MCP client registration starts the command immediately (CVSS 9.8, JFrog, Sep 14, 2026)

Summary

JFrog Security Research (Yuval Moravchick, published September 14, 2026, JFSA-2026-001686326) documents CVE-2026-90898 (CVSS 9.8, critical) in Bifrost (github.com/maximhq/bifrost, an open-source AI gateway/LLM router): its management API lets anyone register an MCP client over HTTP, and a stdio client registration is nothing more than a command plus args — which Bifrost starts immediately inside the gateway process the moment the client is added. No MCP handshake, no authentication (management auth defaults to governance.auth_config.is_enabled=false, i.e. every caller is a local admin), no confirmation. One unauthenticated POST /api/mcp/client runs arbitrary code as the Bifrost process user (appuser on the official image). The HTTP request may time out waiting for the MCP handshake — the timeout does not mean the command failed to start. Fixed in transports/v2.1.0 (unauthenticated stdio registration now returns 403); transports/v2.0.0 and the entire 1.6.x line through 1.6.11 remain vulnerable. Same command-to-subprocess boundary as the recurring MCP stdio pattern; Bifrost is the AI-gateway instance of it, one step worse than Chainlit's sibling flaw because the process launch is unconditional on arrival.

Tags

Vulnerability mechanics

  • Surface: the Bifrost management HTTP API (binary default localhost:8080, commonly exposed). POST /api/mcp/client accepts a JSON body with connection_type: "stdio", a stdio_config object (command + args), and tools_to_execute.
  • Boundary failure: with governance auth off (the shipped default), there is no authentication on the management API at all. The server treats any caller as a local admin.
  • Execution primitive: stdio MCP clients are defined as a program to spawn. Bifrost launches the program when the client is added, before any MCP handshake completes — so the attacker's /bin/sh -c '<payload>' runs as the Bifrost process user even though the POST itself appears to fail/time out. Published PoC: command: "/bin/sh", args: ["-c", "echo PROVEN > /tmp/bifrost-mcp-rce; sleep 60"] → marker file written as the service user.
  • Fix: PR #6757 / commit 12e1703 in transports/v2.1.0 — unauthenticated stdio MCP client registration returns 403 when dashboard authentication is disabled or unconfigured; authenticated admins can still add stdio clients. The fix is not backported: 1.6.x ≤ 1.6.11 and v2.0.0 stay vulnerable.

Defender heuristics

  1. Version-audit the whole line: ≥2.1.0 is fixed; v2.0.0 and 1.6.x through 1.6.11 are not. A "we're on 2.x" answer is not a pass.
  2. Auth is the interim control: set governance.auth_config.is_enabled: true with strong administrator credentials, and keep the management listener off untrusted networks (this flaw makes the management API a remote-code endpoint when reachable).
  3. Treat an exposed pre-2.1.0 instance that ran with auth disabled as compromised: rotate every virtual key and provider credential the gateway holds — an AI gateway concentrates API keys for LLM providers, and in some deployments cloud credentials.
  4. Hunt artifacts: unexpected child processes of the Bifrost/gateway process (shells, downloaders, interpreters) shortly after /api/mcp/client POSTs; review access logs for POST /api/mcp/client from non-administrative source ranges, especially requests that time out (the success signal is the timeout).
  5. Pattern-level: this is the MCP stdio command-execution boundary again (see related pages: Chainlit CVE-2026-45018 allowlist-name-only bypass, marimo notebook-metadata MCP command). The durable rule: an MCP stdio command field reachable over the network is arbitrary code execution — gate the registration endpoint itself, not the command string.

Sibling flaw: CVE-2026-86242 custom-plugin HTTP-path RCE (Sep 6, 2026, Or Peles)

Nine days before the stdio flaw, JFrog's Or Peles published a second unauthenticated RCE in the same productCVE-2026-86242 (CVSS 8.1, JFSA-2026-001684572): Bifrost HTTP transport before 2.0.0 accepts an enabled custom plugin whose path is an HTTP URL through unauthenticated POST /api/plugins (same auth-default-off management API). The shared-object loader treats an http-prefixed path as a download URL, writes the body to a temporary .so, and passes it to Go's plugin.Open; on success Init runs immediately as the Bifrost process user. Loadability gate: the plugin must match the host Go version, OS, architecture, and linkage, and plugin.Open only succeeds on dynamically linked builds (make build DYNAMIC=1, which the vendor requires for custom Go plugins) — on the published statically linked Docker image the same request degrades to SSRF ("Dynamic loading not supported"). The stock bifrost-http binary binds localhost:8080; the official image binds 0.0.0.0 but is static. Fixed in transports/v2.0.0 (PR #5763 / commit e0057ff: refuse create/update of non-builtin plugin paths when the request was let through by disabled/unconfigured dashboard auth + SSRF hardening); the 1.6.x line through 1.6.11 lacks the fix. Practical read: Bifrost exposed two distinct unauthenticated code-execution surfaces in one month on the same management API — treat "which CVE did we fix" as a version-audit question (2.1.0 covers both), and note that both fixes share one design decision: refuse dangerous registrations because the caller was only let through by auth being off.

Sources