Prefab UI’s Generative UI sandbox runs caller-supplied Python inside Pyodide, Python compiled to WebAssembly, inside a Deno subprocess. The sandbox is documented as isolation for untrusted code, including UI text written by an AI model. CVE-2026-120338 shows that isolation failed completely, letting a caller who can submit code read and write files on the host as the server’s own operating system user.
The same write primitive could be turned into a standing backdoor, not just a one-time read. The flaw scores CVSS 3.1 10.0, Critical, and the maintainer closed it in prefab-ui 0.21.0, released 26 October 2026. Operators upgrade now.
A Sandbox Built for AI-Written Interfaces
Prefab is a Python framework for building interactive interfaces. It ships as a native part of FastMCP, the Python toolkit many teams use to serve Model Context Protocol tools and apps. Its Generative UI feature lets an AI agent write the Python that defines a UI component, then run that Python to render it.
Because that code comes from a model, not a trusted developer, Prefab UI runs it inside Pyodide, a WebAssembly build of Python, started inside its own Deno subprocess. The design looks sound on paper.
WebAssembly has no file or network access of its own. Deno is a runtime built to run untrusted JavaScript under an explicit permission system. The two layers should each hold the line on their own.
The Launch Flags That Never Got Restricted
The first break sits in how Prefab UI starts that Deno subprocess. The method PyodideSandbox._start lives in src/prefab_ui/sandbox/_pyodide.py. It launches Deno with --allow-read, --allow-write and --allow-net left unrestricted, open to any file or host the Deno process can reach. Deno’s own permission system, the one safeguard a WebAssembly sandbox depends on here, was switched off from the start.
The second break sits in the runner script that wires Pyodide to that process, src/prefab_ui/sandbox/runner.js. It leaves the Deno API sitting on the JavaScript global object, reachable from any code running in that process. Pyodide’s own js module exists to let Python code call JavaScript, so it proxies that same global object. The Python running inside the WebAssembly sandbox can call js.Deno.readTextFileSync and js.Deno.writeTextFileSync directly, with the same unrestricted permissions Deno started with.
None of this needed a bug in Pyodide, in Deno or in WebAssembly itself. Each layer worked exactly as designed. The flaw is a configuration gap between them: an unrestricted process wrapped in a bridge that was never told to hide it.
- generate_prefab_ui callCallercode argument
- PyodideSandbox starts DenoSandboxunrestricted flags
- Runner leaves Deno on the JS globalSandboxjs global object
- Pyodide js module calls DenoDenoreadTextFileSync
- Host file read or writtenHostas the OS user
- pth file dropped in site-packagesHost
From a File Write to a Standing Backdoor
A file write on its own is a serious problem, but it ends when the process restarts. On prefab-ui 0.20.2, the same write was used to plant a .pth file inside the server’s own Python virtual environment, in its site-packages directory. Python reads every .pth file in that directory on startup and runs any import line it contains.
That means the next Python process that loads the same virtual environment, not only the one running the sandbox, runs the planted import automatically. A single call to generate_prefab_ui turns a one-time file write into code that runs again on every later start of that environment. That is a durable foothold, the kind that normally takes a working privilege escalation chain to get.
Who Could Reach the Sandbox
The common path in is FastMCP’s GenerativeUI provider. It registers an MCP tool named generate_prefab_ui and forwards that tool’s code argument straight into the Pyodide sandbox, with no extra check on what the code does. On a FastMCP server with no authentication configured, any client that can reach the server can call that tool. With authentication turned on, any client allowed to call tools at all can.
Generative UI is an opt-in feature, and the host running it needs Deno installed for the sandbox to start at all. A server that never registers the GenerativeUI provider, or never installs Deno, does not carry this path. That narrows the exposed population to teams who deliberately turned on AI-generated interfaces. That is exactly the part of the market this threat matters most to: companies building or buying agent tooling that renders model output as live, running code.
What a Defender Can Check For
The chain above points straight at what to look for. Check the installed prefab-ui version first; 0.14.0 through 0.20.2 carry the unrestricted launch flags. Check whether any FastMCP server in the environment registers GenerativeUI, and if it does, whether that server requires authentication before a client can call tools.
On a host that already ran an affected version, look at the Python virtual environment behind that FastMCP server. An unfamiliar .pth file in its site-packages directory, especially one added while the server was reachable by an untrusted client, is the sign this specific technique left behind. Process logs showing a Deno child process started with --allow-read, --allow-write and --allow-net together confirm the unrestricted launch the advisory describes.
The Fix in 0.21.0, and What to Do Now
Prefab UI’s maintainer merged the fix as pull request 538, commit 030f074, and shipped it as v0.21.0 on 26 October 2026. After the Deno runtime and Prefab’s own sources finish loading, the runner now revokes Deno’s read, write and network permission before the sandbox accepts any caller-supplied code.
The process also starts with --no-prompt, so a later permission request from inside the sandbox cannot grant those permissions back. The fix also limits the sandbox’s bootstrap network access to cdn.jsdelivr.net alone.
This is not the first time a sandbox meant to hold untrusted code has failed at its own boundary. A VMware ESXi sandbox escape reached a hypervisor host the same way, through a wall nobody had locked down fully.
A separate code injection flaw in openapi-typescript-codegen shows the same pattern: code that was never meant to run free, finding a path to do exactly that. Any team running code it did not write, model-generated or otherwise, is building the same kind of wall and needs to check it the same way.
- Upgrade prefab-ui to 0.21.0 or later on every host that runs the Generative UI sandbox.Closes CVE-2026-120338 by revoking Deno’s file and network permissions before code runs.
- Require authentication on any FastMCP server that registers the GenerativeUI provider.An unauthenticated server lets any client that can reach it call generate_prefab_ui.
- Check site-packages in the affected virtual environment for an unfamiliar .pth file.A planted .pth file is the sign this specific persistence technique was used.
- Turn off the GenerativeUI provider on any server that does not need to render AI-generated code.Removes the remote path into the sandbox entirely until the upgrade is confirmed.
Prefab UI’s own permission model, Deno’s, and Pyodide’s were each sound by themselves. What failed was the agreement between them: the unrestricted launch flags one layer assumed the next would restrict.
Any team that lets a model write code, then runs that code inside a remote code execution sandbox built for that exact purpose, should read this chain as a checklist. It fits one team’s own setup, not just this one framework’s patch note.
