Bifrost’s Off-by-Default Admin Auth Turned an AI Gateway Into Two Unauthenticated RCE Paths

An AI gateway sits between applications and model providers, holding provider keys, virtual keys, routing policy, and — increasingly — Model Context Protocol tool definitions. Two disclosures against maximhq/bifrost show what happens when that component ships with its administration API authenticated by nothing at all.

CVE-2026-90898, published September 14 by JFrog and rated CVSS 3.1 9.8, lets an unauthenticated caller who can reach the Bifrost management listener register an MCP stdio client and cause the gateway to execute an arbitrary command. On September 23 the project published two repository advisories: GHSA-86gf-xh3g-rvxq, the project’s own record of that same stdio path, and GHSA-2qp8-4xgm-fw6g, a separate 8.1 issue in which the plugin API downloads an attacker-supplied Go shared object over HTTP and loads it into the gateway process. Both name Bifrost HTTP transport 2.1.0 as the patched version. Neither advisory reports exploitation in the wild.

The root cause is a default, not a parser bug

Neither finding is a memory-safety error or an input-validation slip. Both advisories quote the same middleware behavior in transports/bifrost-http/handlers/middlewares.go: when the loaded authentication configuration is nil or has IsEnabled false, the request is marked as a local administrator and passed straight to the handler. The documented default is governance.auth_config.is_enabled=false.

That is a fail-open design applied to a network listener. The distinction matters for remediation planning. A parsing flaw is fixed once, upstream, and a version bump ends the exposure. An insecure default is a property of every deployment that never changed it, and the two RCE paths below are consequences rather than independent root causes — the project’s own advisory describes the stdio issue as “an impact chain of the broader insecure-default admin-authentication issue.”

MCP stdio registration is remote process execution by design

An MCP client of type stdio is, definitionally, a command and an argument list that the host starts as a subprocess. Bifrost exposes client registration at POST /api/mcp/client, and the handler copies the request-supplied StdioConfig into the runtime MCP configuration before handing it to the MCP manager. The process launches while the transport initializes.

The consequential detail is that no MCP handshake is required first. JFrog’s advisory notes the HTTP request may time out while Bifrost waits for the subprocess to speak JSON-RPC — but the timeout happens after the command has already run. An operator watching for failed requests would see the failure and miss the execution.

JFrog credits Yuval Moravchick of its Security Research team with the discovery and states the full path was verified on the published maximhq/bifrost:latest image, where an unauthenticated request launched /bin/sh in the container and wrote a marker file as appuser. That is a meaningful scoping note in both directions: it is confirmed execution on a shipped artifact, not a theoretical chain, and it is execution as a non-root container user rather than Docker host root. Real impact then depends on what that container holds — mounted secrets, provider credentials, virtual keys, and reachable internal networks.

The plugin path is narrower, and the advisory says so

GHSA-2qp8-4xgm-fw6g describes a second unauthenticated route to code execution. POST /api/plugins accepts an enabled custom plugin whose path is an HTTP URL; the shared-object loader treats an http prefix as a download instruction, writes the body to a temporary .so, and calls Go’s plugin.Open. If the open succeeds, an optional exported Init function runs immediately with attacker-supplied config.

What keeps this at 8.1 rather than 9.8 is Go’s plugin model. The published Docker image is statically linked, so plugin.Open fails with “Dynamic loading not supported” — on that build class the issue degrades to blind SSRF, since the download itself has no allowlist or private-address restriction. Exploitation as RCE requires a dynamically linked build (DYNAMIC=1), which the vendor documents as the supported way to use custom plugins, and a malicious object matching the target’s Go version, OS/arch, and linkage.

The advisory is unusually candid about the limits of its own testing: the download-then-open behavior was verified on maximhq/bifrost:latest (v1.6.3), but an end-to-end malicious plugin load against a DYNAMIC=1 binary was not completed in the lab. The high attack complexity in the CVSS vector reflects exactly that. Defenders should treat this as a real risk for plugin-capable deployments and as an SSRF primitive everywhere else — not as a second 9.8.

Version ranges need care before you decide you are patched

The advertised remediation is consistent — transport 2.1.0, released September 8 — but the vulnerable ranges are not stated identically across sources, so the safe reading is the conservative one.

The CVE record for CVE-2026-90898 lists all versions below 2.1.0 as affected. JFrog’s advisory adds that transports/v2.0.0 still allows unauthenticated stdio registration and that the 1.6.x line through 1.6.11 does not contain the fix. The GitHub advisories express their Go-ecosystem ranges differently (<1.5.27 for the stdio issue, <1.6.3 for the plugin issue) while both give 2.1.0 as the patched version. Where a machine-readable range and the human-written advisory text disagree, dependency scanners may under-report. Upgrade to 2.1.0 or later and verify the running artifact rather than trusting a scanner’s green result.

What the 2.1.0 fix actually changes

The remediating change is pull request #6757, merged September 2. It adds two pre-registration gates in the MCP handler. rejectStdioMCPClientIfAuthBypassed refuses connection_type: "stdio" with a 403 when the request reached the handler without a genuine credential check. rejectPrivateMCPTargetIfAuthBypassed resolves the hostname of an http or sse client’s connection string and refuses registration if any resolved address is non-public — loopback, RFC1918, link-local, CGNAT, or unspecified — closing an SSRF route toward internal services including the 169.254.169.254 cloud metadata endpoint.

Two things follow from how that fix is written. First, it is a gate keyed on the auth-bypass condition, not a change to the default: authenticated administrators retain both capabilities, because provisioning stdio clients and pointing clients at internal MCP servers are the documented primary use cases. Second, the SSRF hardening is a reminder that this codebase has visited the same neighborhood before — CVE-2026-55245, published in July, covered an incomplete SSRF deny-list in a separate fetch path. Recurring SSRF and authorization defects in one component argue for network-level containment rather than trust in the next patch.

What defenders should do now

  • Upgrade the Bifrost HTTP transport to 2.1.0 or later. Confirm the version inside the running container or process, not only in a manifest or lockfile. Recreate long-lived workloads so no pre-2.1.0 replica survives the rollout.
  • Enable management authentication regardless of version. Set governance.auth_config.is_enabled to true with strong administrator credentials. The 2.1.0 gates reduce what an unauthenticated caller can do; they do not make an anonymous admin API acceptable.
  • Take the management listener off reachable networks. Bind it to loopback or an administrative segment and place authentication and allowlisting in front of any proxy that exposes it. Reachability is the first gate on both findings.
  • Treat an exposed unauthenticated instance as compromised. JFrog’s guidance is explicit: rotate virtual keys and provider credentials. Add any secret mounted into or reachable from that container — cloud role credentials, database strings, internal tokens.
  • Hunt for the specific artifacts. Review management API access logs for POST /api/mcp/client and POST /api/plugins without authentication, unexpected stdio client definitions in the running MCP configuration, child processes of the gateway, temporary .so files, and outbound connections from the gateway to unfamiliar hosts.
  • Check whether your build can load plugins at all. A dynamically linked binary built for custom plugin support moves GHSA-2qp8-4xgm-fw6g from SSRF to code execution. If you do not need custom Go plugins, run the static build.
  • Constrain the gateway process. Non-root user, read-only filesystem where possible, no unnecessary secret mounts, and egress policy that denies link-local metadata endpoints and unrelated internal ranges. Both paths end in “code runs as the gateway process”; that blast radius is yours to set.

The pattern here is one this site keeps meeting: MCP integration points are credentialed control planes, and a tool-registration API is an execution API whether or not it is described that way. The unauthenticated agent control servers disclosed last week and the GitLab MCP transport flaws share Bifrost’s shape — a management surface that assumes a trusted local operator, then listens on a network. Convenient defaults are a deployment decision the vendor makes on the operator’s behalf, and in an AI gateway that decision sits directly on top of every provider credential in the environment.

Sources: