# CVE-2026-14537 — Root Cause Analysis

## Summary

google/mcp-toolbox (repository `googleapis/genai-toolbox`) versions v1.3.0–v1.4.0
suffer from an incorrect-authorization vulnerability (CWE-863). When the server
is configured with an MCP-enabled authorization service (`mcpEnabled: true`,
OAuth scopes via `scopesRequired`) **and** the legacy direct HTTP API is enabled
(`--enable-api`), the legacy endpoint `POST /api/tool/{toolName}/invoke`
executes tools without enforcing the MCP authorization policy. An
unauthenticated remote attacker can invoke tools that the operator believes are
protected by OAuth scopes, because scope enforcement exists only on the `/mcp`
endpoint path.

## Impact

- Component: `internal/server/api.go` (`toolInvokeHandler`) together with
  `internal/server/server.go` (`mcpAuthMiddleware` mounted only under `/mcp`).
- Affected versions: v1.3.0 (2026-05-21) through v1.4.0 (2026-06-04).
- Fixed in: v1.5.0 (2026-06-18), fix commit
  `a6ff910a602adece11f0a6581d6211e5927f7182` ("fix(server): fail if MCP auth
  is enabled together with enable-api (#3435)").
- Risk: high (CVSS 4.0 8.1). Any tool protected solely by the MCP
  authorization model (an `mcpEnabled` authService plus tool-level
  `scopesRequired`, with no legacy `authRequired`) is remotely invocable with
  no credentials at all, including destructive tools (e.g.
  `sqlite-execute-sql`, SQL execution tools against production databases).

## Impact Parity

- Disclosed/claimed maximum impact: authorization bypass — unauthenticated
  remote invocation of scope-protected tools via the direct HTTP API
  (`expected_impact=authz_bypass`, surface `api_remote`).
- Reproduced impact in this run: identical — HTTP 200 and actual tool
  execution (arbitrary SQL against the configured SQLite source) via
  `POST /api/tool/protected-tool/invoke` with no `Authorization` header,
  while the same unauthenticated caller receives HTTP 401 on `/mcp`.
- Parity: `full`.
- Not demonstrated: nothing beyond the claimed impact (no code execution was
  claimed or required).

## Root Cause

Authorization in mcp-toolbox v1.3.0/v1.4.0 is split across two independent
enforcement points:

1. **MCP path** (`/mcp`): `mcpAuthMiddleware` (`internal/server/server.go`)
   validates the Bearer token via `ValidateMCPAuth` (including authService
   `scopesRequired`), and the MCP `tools/call` handler additionally enforces
   tool-level scopes through `mcputil.ValidateScopes(ctx,
   tool.GetScopesRequired(), ...)` (`internal/server/mcp/v20250618/method.go`).
2. **Legacy HTTP API path** (`/api`, enabled by `--enable-api`): the router in
   `internal/server/api.go` has **no** MCP auth middleware, and
   `toolInvokeHandler` only enforces the legacy `authRequired` mechanism
   (`tool.Authorized(verifiedAuthServices)`), which returns `true`
   unconditionally when a tool declares no `authRequired`
   (`IsAuthorized`: "no authorization requirement"). Tool-level
   `scopesRequired` is never consulted on this path.

Consequently, a tool protected only by the modern MCP scope model
(`scopesRequired`, no legacy `authRequired`) is fully open on the legacy HTTP
API: the unauthenticated request passes `IsAuthorized([])` and the tool
executes. The fix in v1.5.0 does not add scope checks to the legacy endpoint;
instead it makes the dangerous configuration fail closed at startup:
`cmd/root.go` and `internal/server/server.go` (`InitializeConfigs`) refuse to
run when any authService `IsMCPEnabled()` and `EnableAPI` are both set
("MCP Auth cannot be enabled together with the legacy HTTP API"), and a new
`IsMCPEnabled()` method was added to the `AuthServiceConfig` interface for
that check.

Fix commit: `a6ff910a602adece11f0a6581d6211e5927f7182` (PR #3435).

## Reproduction Steps

1. Run `bundle/repro/reproduction_steps.sh` (self-contained; installs Go
   1.26.3 if needed, clones/uses the prepared repo cache, builds the real
   product at v1.4.0 and v1.5.0).
2. The script starts a local OIDC authorization-server stub
   (`bundle/repro/oidc_stub.py`), then launches the real v1.4.0 server with
   `bundle/repro/tools.yaml` (generic authService `mcpEnabled: true` +
   `scopesRequired: [read:files]`; tool `protected-tool` of type
   `sqlite-execute-sql` with `scopesRequired: [execute:sql]` and **no**
   `authRequired`) and flags `--enable-api --toolbox-url --port 5000`.
3. It then sends the attacker request
   `POST /api/tool/protected-tool/invoke` with body
   `{"sql": "SELECT 'CVE-2026-14537-PWNED' AS marker"}` and **no**
   `Authorization` header, followed by a contrast request to `/mcp` without a
   token, and finally attempts to start v1.5.0 with the identical config and
   flags.

Expected evidence:

- v1.4.0 legacy API: HTTP 200 with the marker `CVE-2026-14537-PWNED` in the
  JSON response (tool executed unauthenticated) — **vulnerable**.
- v1.4.0 `/mcp`: HTTP 401 with a `WWW-Authenticate` challenge — the MCP path
  enforces authorization correctly.
- v1.5.0: exits at startup logging "MCP Auth cannot be enabled together with
  the legacy HTTP API" and never serves the API — **fixed (fail closed)**.

## Evidence

- `bundle/logs/reproduction_steps.log` — full run transcript.
- `bundle/logs/vuln_server.log` — v1.4.0 server startup (MCP auth + legacy API
  both active).
- `bundle/logs/vuln_api_invoke_status.txt` / `vuln_api_invoke_body.json` —
  HTTP status and response body of the unauthenticated invoke (200 + marker).
- `bundle/logs/vuln_mcp_noauth_status.txt` / `vuln_mcp_noauth_body.json` —
  401 from `/mcp` without a token (contrast control).
- `bundle/logs/fixed_server.log` — v1.5.0 startup refusal message.
- `bundle/logs/oidc_stub.log` — OIDC discovery requests made by the server at
  startup (proves the real MCP auth stack was initialized).
- `bundle/repro/runtime_manifest.json` — structured runtime evidence manifest.

Environment: linux/amd64, Go 1.26.3, mcp-toolbox built from source at tags
v1.4.0 (d67cfbe8ddc) and v1.5.0, python3 OIDC stub on 127.0.0.1:8099.

## Recommendations / Next Steps

- Upgrade to v1.5.0 or later; do not run `--enable-api` together with
  MCP-enabled authorization services on affected versions.
- Operators on v1.3.0/v1.4.0 who must keep the legacy API should add explicit
  legacy `authRequired` entries to every tool (the legacy mechanism is still
  enforced on `/api`), or front the server with a proxy that blocks `/api`.
- Long-term: the legacy `/api` endpoints are deprecated; migrate clients to
  the standard `/mcp` JSON-RPC endpoint where MCP authorization is enforced.

## Additional Notes

- The reproduction is idempotent: the script rebuilds only when the resolved
  commit changes, restarts all services on each run, and cleans up background
  processes via a trap.
- Edge cases: tools that DO declare legacy `authRequired` referencing the
  mcpEnabled generic authService are *not* bypassed on `/api` (claims come
  from the MCP context, which is empty there, yielding 401); the bypass
  applies to tools protected only via the MCP scope model, which is the
  configuration the fix commit targets ("clients could potentially bypass MCP
  authorization policies by using the legacy HTTP API").
- No public PoC existed; this reproduction was built from the fix-commit
  analysis of PR #3435.
