Allow-Read, Allow-Write, Allow-Net: CVE-2026-120338 Gave Every MCP Caller the Keys to Prefab UI Host Files
NVD published CVE-2026-120338 on 1 November 2026, scoring it CVSS 3.1 10.0 (AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H, reporter reefs@jfrog.com). The target is Prefab UI, the generative UI framework from PrefectHQ: its sandbox for executing caller-supplied Python ran inside Pyodide — Python compiled to WebAssembly — in a Deno subprocess, a setup documented as isolation for untrusted code including LLM-written UI. From prefab-ui 0.14.0 through 0.20.2, two configuration choices cancelled both layers. The fix is 0.21.0, and the failure mode should look familiar to anyone who followed the October supply-chain season: the persistence step is a .pth file.
Two layers on paper, none in practice
The NVD description states the mechanics plainly. The launcher started Deno with unrestricted --allow-read, --allow-write and --allow-net, and the runner left the Deno API on the JavaScript global object. Pyodide’s js module proxies that global, so Python code inside the supposed sandbox could call Deno.readTextFileSync and Deno.writeTextFileSync directly against the host. The WebAssembly boundary was intact and irrelevant: the code inside it held a working handle to the outside.
The detail that elevates this from misconfiguration to cautionary tale is in the project’s own comments. The pre-fix runner comment read, in part: the Deno process has read and net access for loading sources, and “No write, env, or FFI access.” The launcher, in the same tree, passed --allow-write. We confirmed both halves first-hand in the fix commit’s diff — the stated threat model and the granted permissions disagreed, and the granted permissions won. A sandbox whose documentation says read-only while its flags say otherwise is not a sandbox with a bug in it. It is a brochure with a sandbox attached.
From file write to every future Python start
Reading host files as the server OS user is bad. Writing them is worse, because Python offers a persistence primitive that predates this entire stack. Per the NVD record, a caller who could supply code could write a .pth file into the server virtualenv’s site-packages and thereby persist code execution: lines starting with import in a .pth file are executed by the site module on every interpreter start in that environment, outside any sandbox.
This is the same primitive that carried the October LiteLLM compromise, where attacker-controlled wheels planted cook.pth so that every Python start executed the payload — a parallel we documented in the 1.107.0 lookalike-fork investigation. The lesson transfers directly: upgrading to 0.21.0 closes the hole but removes nothing an attacker already wrote. If the sandbox was reachable during the exposure window, audit site-packages for unexpected .pth files before declaring the upgrade sufficient.
The remote path is one MCP tool call
The usual remote path, again per the NVD record, is FastMCP’s GenerativeUI provider, which registers the MCP tool generate_prefab_ui and forwards that tool’s code argument into this sandbox. Who can call that tool is the deployer’s authentication story: with no authentication configured — FastMCP’s default — any client able to reach the server can call it, which is the 10.0 case. With authentication, any client allowed to call tools still reaches the flaw, a case JFrog scores at 9.9. Two conditions bound the exposure: Generative UI is opt-in, and the host must have Deno installed. Neither is a control worth leaning on once the feature is enabled.
There is also a quieter delivery path worth naming. The tool executes code the model wrote, so a prompt injection steered through a document or web page can turn an authenticated, well-meaning client into the courier for hostile sandbox input — the same class of exposure behind the AnkiMCP DNS-rebinding and MoLing session-hijack flaws, and adjacent to the scanner-blind MCP flaws in MCP Inspector and the audience confusion in AWS API MCP. Authenticate the caller all you like; if the code argument is model output, the trust boundary sits one prompt injection upstream of the sandbox.
The fix, verified in the diff
The fix is upstream commit 030f074 (pull request 538, committed 25 October 2026), which we read first-hand. After the runtime and Prefab sources finish loading, the runner now revokes Deno read, write and net permissions via Deno.permissions.revoke before accepting generated code, the process starts with --no-prompt so a permission request cannot grant them back, and bootstrap network access is narrowed to cdn.jsdelivr.net:443 with a frozen lockfile. Request/response correlation IDs and stdout/stderr suppression close the remaining response-channel issues in the same commit.
Timing matters here. We confirmed against the PyPI JSON API that 0.21.0 and 0.21.1 both shipped on 26 October 2026 — six days before the CVE was published on 1 November. The patch predates the disclosure, so the exposure question for any deployment is not whether a fix exists but whether the sandbox was reachable while running 0.14.0 through 0.20.2, a window stretching back to March 2026. As of 2 November, exploit trackers show the CVE as disclosed with no public nuclei template, no Metasploit or Exploit-DB entry, and no CISA KEV listing — early, not safe.
What to actually do
- Upgrade to
prefab-ui ≥ 0.21.0. If you run FastMCP with theGenerativeUIprovider, treat this as the top of the queue. - Before or after upgrading, inspect every site-packages directory for unexpected
.pthfiles and any unfamiliar.pyfiles beside them. The upgrade does not clean up persistence. - Do not rely on authentication alone: any principal allowed to call tools could reach the flaw. Review which clients and models may submit
codetogenerate_prefab_ui. - Treat model-written code as untrusted input end to end — the sandbox was the last line of defence, and it was open.
Our verification was read-only. We read the NVD JSON record for CVE-2026-120338 first-hand (published 1 November 2026, CVSS 3.1 10.0, CWE-94/269/693, affected 0.14.0 through 0.20.2, fixed 0.21.0); the full upstream fix commit 030f074 and pull request 538 via the GitHub API, including the _pyodide.py and runner.js diffs quoted above; and the PyPI JSON API for prefab-ui release dates (0.14.0 on 27 March 2026, 0.20.2 on 3 June 2026, 0.21.0 and 0.21.1 on 26 October 2026). The 9.9 authenticated-server score, the JFrog write-up details and the exploitation-tracker state are attributed to secondary coverage (OffSeq Threat Radar, DEV Community and THE D*AI*LY BRIEF write-ups) and the NVD reference list; the GitHub advisory page itself returned 404 to unauthenticated requests and is cited via the NVD reference without a first-hand read. We cloned nothing, ran no sandbox, wrote no .pth file, and contacted no vendor. Brave Search was used for discovery only.
Sources:
- NVD — CVE-2026-120338 (published 1 November 2026; CVSS 3.1 10.0; CWE-94, CWE-269, CWE-693; affected 0.14.0 through 0.20.2, fixed 0.21.0; read first-hand)
- PrefectHQ/prefab commit 030f074 — sandbox permission revocation and runner hardening (PR 538; read first-hand via API)
- PrefectHQ/prefab PR 538 (fix pull request referenced by the NVD record)
- GHSA-595q-jv4r-q7j9 — Prefab UI generative UI sandbox advisory (referenced by NVD; 9.9 authenticated-server score per secondary mirrors)
- PyPI — prefab-ui release history and JSON API (0.21.0 and 0.21.1 upload dates checked 2 November 2026)
- al-ice.ai — The Tag Was Clean: LiteLLM 1.107.0 Was Built From a Lookalike Fork of Its Own Repository
- al-ice.ai — The Tunnel Door Was Left Open on Purpose: CVE-2026-61546 Turns DNS Rebinding Into Unauthenticated AnkiMCP Tool Calls