The Blocklist Named Its Own Default: MCP Context Forge’s CVE-2026-53709 Mints Platform Admins
Two CVEs for IBM’s MCP Context Forge landed at NVD at 15:17Z on 13 October 2026. The serious one is CVE-2026-53709, scored 9.8 on CVSS 3.1 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H) and carrying four CWEs at once — 287 (Improper Authentication), 321 (Hard-coded Cryptographic Key), 798 (Hard-coded Credentials) and 1188 (Insecure Default Initialization). It affects the mcp-contextforge-gateway pip package through 1.0.1, fixed in 1.0.2. The project is a widely-deployed MCP gateway and administrative plane: 4,622 stars, 924 forks, Apache-2.0, actively pushed as recently as today.
The chain is three defaults deep and entirely contained in configuration. The gateway ships a production default of JWT_SECRET_KEY — an HMAC-SHA256 signing key — hardcoded in mcpgateway/config.py and repeated in the project’s own .env.example. The same default deployment sets PLATFORM_ADMIN_EMAIL=admin@example.com and REQUIRE_USER_IN_DB=false. verify_jwt_token() accepts an HS256 token signed with that key, and get_current_user() constructs a platform administrator when the token subject matches the configured admin email and no database row is required. Anyone who has read the public repository can therefore mint a token the gateway accepts as its own administrator, without an account, without credentials, and without touching the login path at all.
The advisory is precise about what that implies operationally, and the point is easy to miss: forged access survives the things you would do about it. Deleting the admin account does not help, because no account is consulted. Rotating the password does not help, because no password is checked. Revoking sessions does not help, because the token is minted fresh each time. The only remediation is changing the signing key, which is the one thing the deployment flow did not require.
The project had already classified this as a vulnerability
What lifts this above the ordinary hardcoded-secret finding is that the codebase had written the answer down before anyone reported it. The reporter quotes the project’s own SECURITY.md, which states that Snyk custom rules “enforce detection of hardcoded JWT secrets and basic auth credentials (CWE-798)” — the project explicitly naming this pattern in its supply-chain policy while shipping it as the configured default for the released package.
And the default string itself appeared in the project’s own weak-secrets blocklist, in the same file that set it. The constant was my-test-key-but-now-longer-than-32-bytes — a name whose history is legible at a glance, a placeholder that was lengthened to clear a 32-character minimum rather than replaced — and the surrounding WEAK_VALUES list enumerated it alongside changeme, secret and password. The validator that consulted the list did the detection correctly and then declined to act on it:
if value.lower() in weak_secrets:
logger.warning(f"SECURITY WARNING - {field_name}: Default/weak secret detected! ...")
...
return v if isinstance(v, SecretStr) else SecretStr(value)
A warning, then the value is returned and used for signing — per the advisory, even with environment="production" and require_strong_secrets=True. This is the recurring failure of the category and it is not an oversight so much as a reflex. Detection was built, the knowledge was present and correct at run time, and the enforcement step was omitted because logging is the safe choice for a library author who does not want to break someone’s boot. Every such decision converts a control into telemetry. The gateway knew its signing key was the one published in its own README-adjacent example file, said so in the log, and signed the token.
The fix is better than the usual one
We read the current configuration code at tag v1.0.2, and it is worth describing because the remediation is more thoughtful than “we changed the default”.
The new default is SecretStr("changeme"). At first glance that looks like a regression — swapping one blocklisted placeholder for a shorter, more notorious one. It is the opposite, and deliberately so. changeme is itself an entry in WEAK_VALUES, which at v1.0.2 has been extracted into a new stdlib-only module, mcpgateway/_security_constants.py, so that the secret-generation script can import it before config loads. And the enforcement that was missing now exists in validate_security_combinations:
if val.lower() in weak_secrets:
if env != "development":
raise SecurityConfigurationError(
f"{field_name}: Weak/default secret rejected in '{env}' environment. "
"Run 'python -m mcpgateway.scripts.init_secrets' to generate strong values."
)
So the shipped default is a value the gateway refuses to start with anywhere except development. The deployment cannot silently inherit a known key, because inheriting it is a startup failure with a remediation command in the message; a parallel branch rejects unset __REPLACE_ME__ placeholders in production. The fix commit, 2af690f8 (22 May 2026), describes itself exactly this way — “Converts the warn-only path in validate_secrets to a hard startup failure for environment != 'development'” — and ships init_secrets.py (145 lines) and 249 lines of tests for it alongside. Choosing a default that is guaranteed to fail closed is a genuinely good piece of design, and it is rarer in this ecosystem than it should be.
Two small blemishes, noted for accuracy rather than as criticism of the fix. The validate_secrets field validator is still warn-only; the hard failure lives in a different method, so the code the advisory quoted is still there doing what it did. And the docstring on validate_security_combinations still opens “Only logs warnings; no changes are made”, which is now wrong on both counts — the method raises, and it assigns csrf_secret_key. Stale docstrings on a security validator are a small thing that misleads the next reader doing exactly what we just did.
The second CVE, and the thing both of them share
CVE-2026-53711, scored 5.1 on CVSS 4.0, is a stored XSS in the Admin UI: the Global Prompts feature saved a prompt template without sufficient output encoding, so markup with a script-capable event handler executed when another user viewed the saved prompt. It is a conventional finding in a consequential place — the viewer is by construction an administrator of an MCP gateway, and the payload runs with whatever that browser session can reach. The advisory, GHSA-x6gc-rm5j-55mw, records it as present from 0.2.0 and fixed in 0.3.1.
Which is where both records become a disclosure-timing story. Trace the dates:
- 22 May 2026 — the JWT fix commit lands.
- 26 May 2026, 22:35Z —
v1.0.2released. - 15 June 2026, 09:25Z and 10:06Z — both GHSAs published, 20 days later.
- 13 October 2026, 15:17Z — both CVE records reach NVD, 120 days after the advisories and 140 days after the fix shipped.
And the XSS is older still: its fixed version, 0.3.1, predates the 1.x line entirely, so a CVE published today describes a defect remediated several minor versions back. Neither record is news to anyone reading the project’s GitHub advisories in June. Both are news to the very large number of organisations whose vulnerability management begins at an NVD feed, and who learned on 13 October that their gateway had a 9.8 against it. We keep writing this paragraph with different product names in it. The GHSA-to-NVD pipeline is where a fixed, disclosed, four-month-old issue goes to become an incident ticket, and the lag is long enough that “published today” has stopped carrying information about when you were exposed.
What to do
Run 1.0.2 or later; at that version a weak signing key is a startup error outside development, so a successful boot is itself evidence. If you cannot upgrade immediately, the single decisive action is to set JWT_SECRET_KEY to a strong unique value — and understand that this is not optional hardening but the only thing that invalidates already-forged tokens. While you are there, set PLATFORM_ADMIN_EMAIL away from admin@example.com and turn REQUIRE_USER_IN_DB on, because the admin bootstrap is the second half of the chain and it is the half that turns a valid signature into an administrator.
Treat any gateway that ran a pre-1.0.2 build on a reachable network with the default key as having had its authentication disabled for that period, and plan accordingly: the forged tokens left no distinguishing trace, so the question “was this used” cannot be answered from the gateway’s own records. Rotating the key is what ends it.
What we verified
Both CVE identifiers, publication timestamps, statuses, CVSS vectors, CWE assignments and assigner fields were read first-hand from the NVD 2.0 API. Both advisories — GHSA-m8rv-5m6m-32ff and GHSA-x6gc-rm5j-55mw — including their summaries, descriptions, severities, affected ranges, patched versions and publication times, were read first-hand from the GitHub security-advisories API for IBM/mcp-context-forge; quoted advisory sentences and the SECURITY.md excerpt are verbatim from that source. The fix commit metadata, message and file list for 2af690f8, the v1.0.2 release timestamp, and the star, fork and push data were read first-hand from the GitHub API. mcpgateway/config.py and mcpgateway/_security_constants.py were fetched at tag v1.0.2 and the code excerpts above are quoted verbatim from them.
We did not install or deploy the gateway, did not forge or test a token, did not run the Admin UI or submit any prompt template, did not execute any proof of concept, and sent no request to any third-party deployment. The pre-1.0.2 code is described as the advisory describes it; our own source reading was done at the fixed tag.
Sources:
- NVD — CVE-2026-53709, MCP Context Forge hardcoded JWT signing key and platform-admin forgery (9.8, CWE-287/321/798/1188)
- NVD — CVE-2026-53711, stored XSS in the Global Prompts feature of the MCP Admin UI (5.1 CVSS 4.0)
- GHSA-m8rv-5m6m-32ff — hardcoded default
JWT_SECRET_KEYenabling pre-authentication admin token forgery (15 June 2026) - GHSA-x6gc-rm5j-55mw — stored XSS in mcp-context-forge Admin UI (15 June 2026)
- Fix commit
2af690f8(22 May 2026) — raise on weak JWT key outside development, plusinit_secrets.py - IBM/mcp-context-forge release v1.0.2 (26 May 2026)
- mcpgateway
config.pyatv1.0.2— thechangemedefault andvalidate_security_combinations - mcpgateway
_security_constants.pyatv1.0.2— theWEAK_VALUEStuple