{"repro_id":"REPRO-2026-00318","version":6,"title":"mcp-toolbox authorization bypass: unauthenticated tool invocation via direct HTTP API","repro_type":"security","status":"published","severity":"high","description":"Incorrect authorization in google/mcp-toolbox v1.3.0–v1.4.0 (CWE-863, CVSS 4.0 8.1). Affected versions: >=1.3.0 (2026-05-21) and <=1.4.0 (2026-06-04); fixed in v1.5.0 (2026-06-18).","root_cause":"# CVE-2026-14537 — Root Cause Analysis\n\n## Summary\n\ngoogle/mcp-toolbox (repository `googleapis/genai-toolbox`) versions v1.3.0–v1.4.0\nsuffer from an incorrect-authorization vulnerability (CWE-863). When the server\nis configured with an MCP-enabled authorization service (`mcpEnabled: true`,\nOAuth scopes via `scopesRequired`) **and** the legacy direct HTTP API is enabled\n(`--enable-api`), the legacy endpoint `POST /api/tool/{toolName}/invoke`\nexecutes tools without enforcing the MCP authorization policy. An\nunauthenticated remote attacker can invoke tools that the operator believes are\nprotected by OAuth scopes, because scope enforcement exists only on the `/mcp`\nendpoint path.\n\n## Impact\n\n- Component: `internal/server/api.go` (`toolInvokeHandler`) together with\n  `internal/server/server.go` (`mcpAuthMiddleware` mounted only under `/mcp`).\n- Affected versions: v1.3.0 (2026-05-21) through v1.4.0 (2026-06-04).\n- Fixed in: v1.5.0 (2026-06-18), fix commit\n  `a6ff910a602adece11f0a6581d6211e5927f7182` (\"fix(server): fail if MCP auth\n  is enabled together with enable-api (#3435)\").\n- Risk: high (CVSS 4.0 8.1). Any tool protected solely by the MCP\n  authorization model (an `mcpEnabled` authService plus tool-level\n  `scopesRequired`, with no legacy `authRequired`) is remotely invocable with\n  no credentials at all, including destructive tools (e.g.\n  `sqlite-execute-sql`, SQL execution tools against production databases).\n\n## Impact Parity\n\n- Disclosed/claimed maximum impact: authorization bypass — unauthenticated\n  remote invocation of scope-protected tools via the direct HTTP API\n  (`expected_impact=authz_bypass`, surface `api_remote`).\n- Reproduced impact in this run: identical — HTTP 200 and actual tool\n  execution (arbitrary SQL against the configured SQLite source) via\n  `POST /api/tool/protected-tool/invoke` with no `Authorization` header,\n  while the same unauthenticated caller receives HTTP 401 on `/mcp`.\n- Parity: `full`.\n- Not demonstrated: nothing beyond the claimed impact (no code execution was\n  claimed or required).\n\n## Root Cause\n\nAuthorization in mcp-toolbox v1.3.0/v1.4.0 is split across two independent\nenforcement points:\n\n1. **MCP path** (`/mcp`): `mcpAuthMiddleware` (`internal/server/server.go`)\n   validates the Bearer token via `ValidateMCPAuth` (including authService\n   `scopesRequired`), and the MCP `tools/call` handler additionally enforces\n   tool-level scopes through `mcputil.ValidateScopes(ctx,\n   tool.GetScopesRequired(), ...)` (`internal/server/mcp/v20250618/method.go`).\n2. **Legacy HTTP API path** (`/api`, enabled by `--enable-api`): the router in\n   `internal/server/api.go` has **no** MCP auth middleware, and\n   `toolInvokeHandler` only enforces the legacy `authRequired` mechanism\n   (`tool.Authorized(verifiedAuthServices)`), which returns `true`\n   unconditionally when a tool declares no `authRequired`\n   (`IsAuthorized`: \"no authorization requirement\"). Tool-level\n   `scopesRequired` is never consulted on this path.\n\nConsequently, a tool protected only by the modern MCP scope model\n(`scopesRequired`, no legacy `authRequired`) is fully open on the legacy HTTP\nAPI: the unauthenticated request passes `IsAuthorized([])` and the tool\nexecutes. The fix in v1.5.0 does not add scope checks to the legacy endpoint;\ninstead it makes the dangerous configuration fail closed at startup:\n`cmd/root.go` and `internal/server/server.go` (`InitializeConfigs`) refuse to\nrun when any authService `IsMCPEnabled()` and `EnableAPI` are both set\n(\"MCP Auth cannot be enabled together with the legacy HTTP API\"), and a new\n`IsMCPEnabled()` method was added to the `AuthServiceConfig` interface for\nthat check.\n\nFix commit: `a6ff910a602adece11f0a6581d6211e5927f7182` (PR #3435).\n\n## Reproduction Steps\n\n1. Run `bundle/repro/reproduction_steps.sh` (self-contained; installs Go\n   1.26.3 if needed, clones/uses the prepared repo cache, builds the real\n   product at v1.4.0 and v1.5.0).\n2. The script starts a local OIDC authorization-server stub\n   (`bundle/repro/oidc_stub.py`), then launches the real v1.4.0 server with\n   `bundle/repro/tools.yaml` (generic authService `mcpEnabled: true` +\n   `scopesRequired: [read:files]`; tool `protected-tool` of type\n   `sqlite-execute-sql` with `scopesRequired: [execute:sql]` and **no**\n   `authRequired`) and flags `--enable-api --toolbox-url --port 5000`.\n3. It then sends the attacker request\n   `POST /api/tool/protected-tool/invoke` with body\n   `{\"sql\": \"SELECT 'CVE-2026-14537-PWNED' AS marker\"}` and **no**\n   `Authorization` header, followed by a contrast request to `/mcp` without a\n   token, and finally attempts to start v1.5.0 with the identical config and\n   flags.\n\nExpected evidence:\n\n- v1.4.0 legacy API: HTTP 200 with the marker `CVE-2026-14537-PWNED` in the\n  JSON response (tool executed unauthenticated) — **vulnerable**.\n- v1.4.0 `/mcp`: HTTP 401 with a `WWW-Authenticate` challenge — the MCP path\n  enforces authorization correctly.\n- v1.5.0: exits at startup logging \"MCP Auth cannot be enabled together with\n  the legacy HTTP API\" and never serves the API — **fixed (fail closed)**.\n\n## Evidence\n\n- `bundle/logs/reproduction_steps.log` — full run transcript.\n- `bundle/logs/vuln_server.log` — v1.4.0 server startup (MCP auth + legacy API\n  both active).\n- `bundle/logs/vuln_api_invoke_status.txt` / `vuln_api_invoke_body.json` —\n  HTTP status and response body of the unauthenticated invoke (200 + marker).\n- `bundle/logs/vuln_mcp_noauth_status.txt` / `vuln_mcp_noauth_body.json` —\n  401 from `/mcp` without a token (contrast control).\n- `bundle/logs/fixed_server.log` — v1.5.0 startup refusal message.\n- `bundle/logs/oidc_stub.log` — OIDC discovery requests made by the server at\n  startup (proves the real MCP auth stack was initialized).\n- `bundle/repro/runtime_manifest.json` — structured runtime evidence manifest.\n\nEnvironment: linux/amd64, Go 1.26.3, mcp-toolbox built from source at tags\nv1.4.0 (d67cfbe8ddc) and v1.5.0, python3 OIDC stub on 127.0.0.1:8099.\n\n## Recommendations / Next Steps\n\n- Upgrade to v1.5.0 or later; do not run `--enable-api` together with\n  MCP-enabled authorization services on affected versions.\n- Operators on v1.3.0/v1.4.0 who must keep the legacy API should add explicit\n  legacy `authRequired` entries to every tool (the legacy mechanism is still\n  enforced on `/api`), or front the server with a proxy that blocks `/api`.\n- Long-term: the legacy `/api` endpoints are deprecated; migrate clients to\n  the standard `/mcp` JSON-RPC endpoint where MCP authorization is enforced.\n\n## Additional Notes\n\n- The reproduction is idempotent: the script rebuilds only when the resolved\n  commit changes, restarts all services on each run, and cleans up background\n  processes via a trap.\n- Edge cases: tools that DO declare legacy `authRequired` referencing the\n  mcpEnabled generic authService are *not* bypassed on `/api` (claims come\n  from the MCP context, which is empty there, yielding 401); the bypass\n  applies to tools protected only via the MCP scope model, which is the\n  configuration the fix commit targets (\"clients could potentially bypass MCP\n  authorization policies by using the legacy HTTP API\").\n- No public PoC existed; this reproduction was built from the fix-commit\n  analysis of PR #3435.\n","cve_id":"CVE-2026-14537","cwe_id":"CWE-863 (Incorrect Authorization)","source_url":"https://nvd.nist.gov/vuln/detail/CVE-2026-14537","package":{"name":"googleapis/genai-toolbox","ecosystem":"github","affected_versions":">= v1.3.0 (2026-05-21) and <= v1.4.0 (2026-06-04)","fixed_version":"v1.5.0 (2026-06-18)"},"reproduced_at":"2026-08-01T05:51:48.163499+00:00","duration_secs":2266.0,"tool_calls":197,"handoffs":2,"total_cost_usd":4.336807,"agent_costs":{"claim_matcher":0.013634,"judge":0.462673,"learning_policy":0.010675,"repro":2.60939,"support":0.047035,"vuln_variant":1.1934},"cost_breakdown":{"claim_matcher":{"gpt-5.4-mini-2026-03-17":0.013634},"judge":{"gpt-5.6-sol":0.462673},"learning_policy":{"gpt-5.4-mini-2026-03-17":0.010675},"repro":{"accounts/fireworks/models/kimi-k3":2.60939},"support":{"accounts/fireworks/models/kimi-k3":0.047035},"vuln_variant":{"accounts/fireworks/models/kimi-k3":1.1934}},"quality":{"confidence":"high","idempotent_verified":false,"community_verifications":0},"evidence":{"workflow":{"profile":"known_vulnerability","schema_version":2,"stages":["support","claim_contract","repro","judge","vuln_variant"]}},"environment":{"sandbox_image":"ghcr.io/n3mes1s/pruva-sandbox@sha256:8096b2518d6022e13d68f885c3b8ded6b4fe607098b1a1ccbfb99abc004d1dc1"},"published_at":"2026-08-01T05:51:48.668243+00:00","retracted":false,"artifacts":[{"path":"bundle/repro/rca_report.md","filename":"rca_report.md","size":7290,"category":"analysis"},{"path":"bundle/repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":11390,"category":"reproduction_script"},{"path":"bundle/logs/fixed_server.log","filename":"fixed_server.log","size":303,"category":"log"},{"path":"bundle/logs/oidc_stub.log","filename":"oidc_stub.log","size":161,"category":"log"},{"path":"bundle/logs/reproduction_steps.log","filename":"reproduction_steps.log","size":1564,"category":"log"},{"path":"bundle/logs/vuln_api_invoke_body.json","filename":"vuln_api_invoke_body.json","size":53,"category":"other"},{"path":"bundle/logs/vuln_api_invoke_status.txt","filename":"vuln_api_invoke_status.txt","size":4,"category":"other"},{"path":"bundle/logs/vuln_mcp_noauth_body.json","filename":"vuln_mcp_noauth_body.json","size":85,"category":"other"},{"path":"bundle/logs/vuln_mcp_noauth_status.txt","filename":"vuln_mcp_noauth_status.txt","size":4,"category":"other"},{"path":"bundle/logs/vuln_server.log","filename":"vuln_server.log","size":3343,"category":"log"},{"path":"bundle/repro/oidc_stub.py","filename":"oidc_stub.py","size":2467,"category":"script"},{"path":"bundle/repro/runtime_manifest.json","filename":"runtime_manifest.json","size":824,"category":"other"},{"path":"bundle/repro/tools.yaml","filename":"tools.yaml","size":1142,"category":"other"},{"path":"bundle/repro/validation_verdict.json","filename":"validation_verdict.json","size":868,"category":"other"}]}