Skip to the content.

IBM/mcp-context-forge - security scan

Repository: IBM/mcp-context-forge Commit scanned: 50602d6dc1c217ed60f7ac070f2a5210f53181a2 Version: 1.0.5 (beta) Scan date: 2026-07-17 Disclosure status: public — post-only (strict-norm: IBM PSIRT private-reporting policy; 0 real findings)

Summary

Severity Count
Critical 0
High 709
Medium 237
Low 0
Info 0

Total findings: 946 (0 real after curation)

This is the 18th clean scan in the series, and the one with the richest attack surface so far. ContextForge (aka MCP Gateway) is an AI gateway, registry, and reverse proxy that sits in front of any MCP, A2A, or REST/gRPC backend — it authenticates callers, forwards credentials to upstream servers, proxies tool invocations out to registered endpoints, discovers JWKS keys, and runs a plugin system. That is the credential-proxy class (last seen in soju06/codex-lb) scaled up to a full multi-protocol gateway. Every finding that would be real on a project like this — SSRF from the proxy egress, an auth-bypass in the token path — turns out to be defended, by design, secure-by-default.

The finding that matters on a gateway: SSRF egress — and it’s defended

A gateway’s whole job is to take a caller’s request and fetch something else on their behalf. So the finding class that matters is server-side request forgery: can a registered backend URL, a tool argument, or a JWKS discovery URL be pointed at 169.254.169.254 (cloud metadata) or an internal host? On a2aproject/a2a-python last week this check came back negative — the push-notification sender had no egress validation at all (mitigated only by the finding’s blind/authenticated shape). Here it comes back thoroughly positive.

ContextForge ships a dedicated SecurityValidator (mcpgateway/common/validators.py, ~2,300 lines) whose validate_url / _validate_ssrf runs on a percent-decoded copy of the URL, rejects IPv6-literal and protocol-relative forms and JS-style \uXXXX escapes, and then checks the resolved host against a configurable SSRF policy. The defaults are the point:

The validator is wired into every egress path that matters — the HTTP client service, the A2A service, the OAuth/JWKS manager, the OpenAPI service, the LLM proxy — and there’s a separate test_ssrf_redirect_protection.py covering the redirect-follow bypass, plus a Rust backend_url_validator.rs for the runtime crate. A static scanner can flag a missing guard; it structurally can’t credit one, which is why a rich surface like this reads as 946 findings until you trace the egress by hand.

The auth path: 11 “unverified JWT decode” highs, all the peek-then-verify pattern

The next thing that looks alarming on a gateway is jwt.decode(token, options={"verify_signature": False}) — 11 of them, in verify_credentials.py, routers/auth.py, token_validation_service.py, and the SSO/email-auth routers. On an auth boundary, an unverified decode used to grant access is a textbook bypass. None of these do that:

The main verification path (verify_credentials.py:325) builds proper verify_signature / verify_exp / verify_aud options and is the one that actually gates access.

Where the other 900 findings come from

Patterns observed

The lesson this scan sharpens is that surface richness and finding count are not signal. ContextForge has the widest genuinely-dangerous surface I’ve scanned — it proxies arbitrary backends, forwards OAuth tokens, discovers remote keys, and runs plugins — and it produced 946 findings, more than any clean scan before it. Yet the two questions that actually decide a gateway’s security posture (is the egress SSRF-guarded? is the token path verified before it grants access?) both resolve to “yes, by default,” and you can only learn that by reading the code. The count is dominated by a scanner re-flagging another scanner’s own baseline file.

It’s also a study in what a mature security program looks like from the outside: a 43 KB SECURITY.md that tells you to disable the Admin UI in production and audit the code yourself; pre-commit gates running Bandit, Semgrep, Dodgy, and detect-secrets; Cosign keyless-OIDC signing and attestation on release images; secure-by-default SSRF config where you have to opt in to reach localhost or RFC 1918. The right way to read a project like this is not “709 highs” but “which of these defenses would I have to disable to get hurt” — and here the answer is all of them, deliberately, with flags that default to safe.

Notes on the tool

Each of these maps to an AI PatchLab backlog item:

Disclosure timeline

Reproduce

python scanner/run_scan.py \
  --from-git-url "https://github.com/IBM/mcp-context-forge" \
  --reports-dir ./reports/ibm-mcp-context-forge \
  --min-severity medium --ignore-samples