high · 8.1 Patched

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

CVE-2026-54446 · GHSA-x9vc-9ffq-p3gj · published 14 Jul 2026

How serious is it

  • Severity: high, CVSS score 8.1 out of 10.
  • No exploit prediction (EPSS) score yet.
  • Not on the CISA list of exploited vulnerabilities.
  • No public exploit in Exploit-DB.

Is your app affected by CVE-2026-54446?

Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.

Who is affected

  • netlicensing-mcp (PyPI) <= 0.1.5

How to check

  1. Run pip show netlicensing-mcp in your project to see the installed version, including copies pulled in by other packages.
  2. Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
  3. Or run npx shippable-dev scan localhost:3000 in your project folder. It reads your lockfile and flags this vulnerability along with every other known one.

How to fix

Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
  1. Upgrade netlicensing-mcp to 0.1.6 or later, commit the lockfile and redeploy.
  2. If a vulnerable version was live, check your logs and rotate the secrets that server could read.

The advisory

## 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/cor…

Check your whole app

Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.

References

Merged from GitHub Advisory Database. Last updated 24 Oct 2026. All vulnerabilities · RSS · JSON