A Traversal That Picks Its Own Serializer — NetLicensing MCP’s ../token Redaction Bypass (CVE-2026-57496) Plus a WordPress Draft-Disclosure Twin

Two CVEs published within a day of each other describe the same defect wearing different clothes: an MCP tool that trusts caller input one layer too late. In the NetLicensing MCP server, a product number of ../token survives string interpolation into an HTTP client that silently normalizes it onto the token endpoint — and because the response arrives wearing a product’s label, it passes through the serializer that never learned to redact secrets. In a WordPress MCP plugin, a post-listing tool registered with an empty capability requirement honors status=all and hands every draft, pending and scheduled post to subscriber-level callers. Both are authorization checks written per call site rather than per boundary — the exact shape we dissected in the holaOS Telegram connector and the PortKey gateway SSRF earlier this month.

CVE-2026-57496: the URL library finishes the attacker’s sentence

NetLicensing MCP Server is a natural-language interface that lets agentic applications manage the software-licensing lifecycle in Labs64 NetLicensing — products, tokens, API keys. The netlicensing_get_product tool took its product_number argument and interpolated it straight into a REST path, as the advisory quotes from tools/products.py:22: nl_get(f"/product/{product_number}"). Passing ../token produces /product/../token, and httpx applies WHATWG dot-segment normalization and sends the request to /token instead. No exotic parser bug, no encoding trick — the HTTP client did exactly what the URL standard says, and the application handed it a path it had never validated.

The traversal alone would be a wrong-endpoint bug. What makes it critical — 9.6 CVSS 3.1, network, low complexity, no privileges, scope changed, confidentiality and integrity high — is what happens on the way back. The token endpoint’s response is wrapped as a "Product" through the generic _wrap_json serializer rather than the token-specific _wrap_json_token_read, so redact_token_read() — the only code that masks APIKEY number and SHOP shopURL fields — is never invoked. The MCP tool output returns the raw API key value verbatim, including admin-level ROLE_APIKEY_ADMIN credentials, even embedding the secret in a console_url field. The advisory’s proof output shows it plainly: "number": "actual-api-key-value-5678" next to "role": "ROLE_APIKEY_ADMIN". An authenticated MCP client holding a non-administrative key walks away with administrative ones. The redaction existed, was tested, and worked — it just lived on a code path the attacker’s input could route around by choosing which endpoint answered.

The fix, which we read in full, is the right shape: a centralized _validated_path() guard in client.py applied in all four HTTP helpers (nl_get, nl_post, nl_put, nl_delete), unquoting each segment first — because httpx does not decode %2f/%2e, so encoded separators would otherwise sail through — and rejecting dot segments, embedded separators and control characters with a 400 before any request is built. Twenty-two regression tests cover the advisory PoC, encoded separators, backslashes and unrooted paths. Two timeline facts deserve recording. First, the fix (PR #6, by first-time contributor @r-brown) merged and released as 0.1.8 on 18 June 2026, while the CVE record only appeared on 15 October — a four-month window in which the patch existed and the identifier did not, the exact inversion of the usual “CVE first, patch later” complaint. Second, the commit message carries a Co-authored-by: Claude Opus 4.8 trailer with a Claude Code session link: the guard that closed an AI-tooling vulnerability was itself written with AI assistance, reviewed in by a human maintainer. The advisory credits reporter EQSTLab with analyst yym8538, is rated critical by GitHub, carries the pip coordinate netlicensing-mcp (≤ 0.1.5 vulnerable, 0.1.8 patched), and — unusually for this feed — resolves cleanly in OSV with PyPI ecosystem ranges and a PYSEC alias. The scanner story here is the good one, for once.

CVE-2026-109382: the list endpoint nobody guarded

The WSP MCP AI Agents Connector plugin for WordPress exposes posts to AI agents through MCP tools. Its authenticate() method, which we read at tag 2.9.5, returns true for any logged-in user — the first branch is literally if (is_user_logged_in()) return true;, reachable by a subscriber authenticating with an application password. The static API key and OAuth paths below it are careful work: constant-time comparisons, admin mapping, token-to-user binding. None of that matters when the front door waves everyone through and the per-tool checks are supposed to carry the weight.

They do — except once. The single-post callback wsp_execute_get_post() checks current_user_can('read_post', $id) before returning content. The list callback wsp_execute_get_posts(), registered with an empty capability requirement, honors a caller-supplied status=all argument that expands into a query over publish, draft, pending and future posts with no author or ownership filter. A subscriber-level caller reads the titles, URLs, authors, categories, tags and 40-word excerpts of every unpublished post on the site — CVSS 6.5, CWE-863, assigned by Wordfence, published 16 October. The guard existed twelve lines away in the sibling function. This is the same “two hand-written lists” failure we have now seen in EFAK, 99AI and half the October batches: the single-item path got the check, the bulk path did not, and the bulk path is the one an agent calls in a loop.

The 2.9.6 fix, also read first-hand, is a capable piece of WordPress authorization work rather than a one-line bandage: users without edit_posts are forced to published-only; status=all survives only for editors; and contributors without edit_others_posts get an author__in scope plus a two-part merge that unions everyone’s published posts with their own unpublished ones. Subscribers see the public site, nothing more. Notably, OSV has no record of this CVE at time of writing — WordPress plugins are not a coordinated ecosystem there — so no dependency scanner will surface it; WordPress administrators find it through Wordfence intelligence or not at all.

The pattern to grep for

Both flaws share a root cause worth stating as a rule: the security property lived downstream of attacker-controlled routing. In NetLicensing, redaction was a property of the serializer the request was supposed to reach; in WSP, authorization was a property of the callback that was supposed to be the only reader. In both cases the attacker picked a different route to the same data and the property did not follow. Audit MCP servers for exactly this: every tool argument interpolated into a path, URL or query must be validated where it enters, not where it was expected to go — and every list endpoint needs the check its single-item sibling has, because agents enumerate.

What to do

  • Upgrade netlicensing-mcp to 0.1.8 and rotate every NetLicensing API key the server has ever held. A recovered admin token grants broad read, write and delete over the licensing account; the traversal leaves no distinctive error since the request succeeds normally.
  • Upgrade the WSP MCP AI Agents Connector past 2.9.5 (2.9.6 carries the fix) and treat drafts as disclosed. Excerpts, titles and authors of unpublished posts were readable by any authenticated user; review who held subscriber-level credentials during the exposure window.
  • Validate identifiers before interpolation, in the HTTP layer, not per tool. The NetLicensing fix covers all four helpers from one guard — per-tool validation rots as new tools are added, which for MCP servers is weekly.
  • Diff your list endpoints against your single-item endpoints. If get_post checks a capability and get_posts does not, you have this bug. Agents call the plural first.
  • Do not rely on CVE feeds for WordPress MCP plugins. CVE-2026-109382 is absent from OSV; track Wordfence threat intelligence for the WordPress fleet instead.

Verification note: CVE IDs, timestamps, CVSS vectors, CWEs, vulnStatus and source identifiers for both records were read first-hand from the NVD 2.0 API. The NetLicensing advisory text, severity, pip coordinate, affected range, credits and publish timestamp were read first-hand via the GitHub repository security-advisory API; the fix commit message, author, date and complete diff (including the Claude co-authorship trailer) were read first-hand from the commit patch; release 0.1.8 metadata from the releases API; OSV and global-advisory lookups measured directly. The WSP authenticate() logic, both post callbacks and the 2.9.6 fix were read first-hand from the WordPress.org SVN tags 2.9.5 and 2.9.6; the status=all expansion and empty capability detail per the NVD/Wordfence description. Pipecat CVE-2026-102682 (dev-runner exposure, medium, by design) was reviewed and deliberately left out as below the bar. Nothing installed, no MCP server run, no traversal string sent anywhere except reading the cited public records; no PoC reproduced.

Sources: