







Agent frameworks increasingly execute code that comes from a model, a user, or a combination of both. The code may call APIs, inspect files, transform data, or invoke tools. It therefore needs an execution environment with a clearly defined set of permissions. The same need appears outside agent systems: workflow platforms run user-defined transforms, spreadsheets evaluate formulas, CI systems run tests from dependencies, and desktop applications load plugins or embedded code.
For many Python-facing products, Pyodide is an attractive implementation choice. It runs CPython in WebAssembly and can be embedded directly into a JavaScript application. Product teams then commonly restrict imports such as os, subprocess, and js before evaluating user code. n8n, Grist, smolagents, LangChain's sandbox, stlite, and cibuildwheel all used variants of this model.
Our review focused on whether those Python-level restrictions matched the capabilities available to the embedded interpreter. Across the seven targets, they did not.
A sandbox allows code to run while limiting the resources it can access. The limit can be enforced at several layers:
Calling something a sandbox does not make it safe. The restriction has to block the ways code can actually reach the host. In these products, Python was prevented from importing some modules, but ctypes could still call functions exposed by Emscripten.
Pyodide is CPython compiled to WebAssembly through Emscripten. It runs a complete Python 3.x interpreter - with the full standard library - inside any JavaScript runtime: browsers, Node.js, or Deno.

Figure 1: How Pyodide compiles CPython into WebAssembly and JavaScript glue that runs inside a browser, Node.js, or Deno host.
Emscripten does not just compile C to WASM. It generates a JavaScript "glue" layer that gives the compiled code access to the outside world. This includes:
One of Pyodide’s main selling points is how little code an application needs to embed it.
This in-process model is operationally convenient: there is no separate Python service or container to operate. Products often add an import denylist for os, subprocess, sys, and the js bridge module before they run untrusted code.
Pyodide includes CPython's ctypes module, which provides a Foreign Function Interface to C libraries. The binary can also expose Emscripten symbols unless they are explicitly stripped at link time.
When a product loads Pyodide and runs untrusted Python, the following are available inside the "sandbox":
In the configurations we tested, ctypes remained available and the relevant symbols could be resolved. WASM memory isolation still protects the module's linear memory; it does not itself limit calls that the Emscripten host has intentionally made available.
The modules and Emscripten interfaces described above do not run on their own: they sit inside a product deployment with three layers. Separating them makes it easier to see where a product applies restrictions and which components are involved at runtime.

Figure 2: The layers of a Pyodide deployment. Product policy controls untrusted Python, while Pyodide, Emscripten, the host runtime, and the execution environment determine the capabilities and data that remain reachable.
The products in this research applied most of their policies at Layer 1. The next section examines the Python and Emscripten interfaces that remained available below those policies.
Python's ctypes module provides a Foreign Function Interface to C libraries. ctypes.CDLL(None) loads the main program's symbol table - equivalent to dlopen(NULL) in C. In a normal CPython installation, this gives access to libc.
In Pyodide, "the main program" is the Emscripten-compiled WASM module. CDLL(None) returns the entire Emscripten export table: every C function that was linked into the binary and not stripped. This includes:
| Symbol | What It Does |
|---|---|
| emscripten_run_script | Calls eval() in the host JS runtime (void return) |
| emscripten_run_script_string | Calls eval() in the host JS runtime (returns string) |
| emscripten_run_script_int | Calls eval() in the host JS runtime (returns int) |
| system | Executes a shell command via Emscripten's syscall emulation |
| popen | Opens a pipe to a process |
| execve | Replaces the current process image |
The emscripten_run_script functions are the relevant primitives in this chain. They pass a C string to the host JavaScript runtime and run it as JavaScript. Depending on the variant, they can also return a value to the caller. Calling one from Pyodide crosses from the WASM module into the embedding process.

Figure 3: The ctypes escape path from untrusted Python through exported Emscripten functions into JavaScript host APIs and the resources available to the host process.
ctypes was available in all seven configurations we tested. It is part of CPython's standard library, so a deployment must deliberately remove or restrict it if it does not want user code to use it.
Import restrictions also need to account for Python's reflective object model. Every object inherits from object, and object.__subclasses__() exposes classes that may retain references to the full __builtins__ namespace:
This expression walks from an empty tuple to object, enumerates subclasses, finds warnings.catch_warnings, and uses that class's module reference to recover __builtins__ and __import__. Depending on how the product implements its import restrictions, that can provide an alternative way to load a module.
catch_warnings is one example, not a complete list of possible routes. Python's runtime object graph is large, and it changes with the Python or Pyodide version and with the modules the application has already loaded. In a particular deployment, another class, function, module reference, or cached global may provide a similar route to imports or builtins. Blocking one known traversal should not be treated as proof that all reflection-based routes are closed.
This is why a denylist for a few module names is difficult to treat as a complete security boundary. The interpreter has multiple module references, reflection APIs, and internal execution paths. A useful short-term control can reduce exposure, but it needs to be combined with a smaller available surface and an independent host-level restriction.
The products blocked some Python modules and functions, but the same actions were still available through lower-level runtime interfaces.
For example, os.system is a Python wrapper around a lower-level command-execution capability. Replacing that wrapper does not prevent code from reaching the same capability through FFI when the underlying symbol remains available.
The available mitigations provide different levels of assurance. An import allowlist and removal of ctypes reduce the Python attack surface. Stripping unneeded Emscripten exports reduces the symbols an FFI caller can resolve. Process isolation, OS controls, and tightly scoped host-runtime permissions provide a separate boundary if the language runtime is bypassed.
The Pyodide-to-host transition was similar across the targets. The consequences varied with the host runtime, the permissions of its process, and the secrets or data accessible from that process.
Node.js applications commonly make filesystem, process, and environment APIs available to their own code. When the escaped JavaScript runs in that context, it can use the same APIs as the embedding process:
Node.js does not normally require a per-API permission prompt. Four targets - n8n, Grist, cohere-terrarium, and stlite - embedded Pyodide in Node.js contexts, so the impact reflected the privileges of their respective processes.
Deno requires explicit permissions for operations such as subprocess spawning (--allow-run), filesystem access (--allow-read), and network access (--allow-net).
Products may grant some of these permissions to implement their features. For example, smolagents launched Deno with --allow-run so an agent could execute tools. Grist's Deno migration included --allow-env, which made environment variables available to code running with that permission.
These permissions influence what escaped JavaScript can do after it reaches Deno. They do not prevent the transition from Pyodide into the Deno runtime, but they can limit the operations available after that transition.
In Node.js, the embedding process normally has access to APIs such as require(), fs, child_process, and process.env. In Deno, comparable process, filesystem, network, and environment APIs are controlled by the --allow-* permissions granted to the process. The JavaScript runtime and its permissions determine which operations are available after code reaches the host.
The host runtime determines what an escaped script can do. The surrounding environment determines what that script can reach. A Node.js process running a local development tool and a Node.js process running a release pipeline may expose the same APIs, but they do not hold the same data or credentials.
Build systems run tests and build steps alongside release infrastructure. Depending on the configuration, the runner may have a source checkout, GITHUB_TOKEN, PyPI upload tokens, GPG signing keys, or artifacts that are about to be published. The Pyodide host may be Node.js or another runtime; the distinctive risk is the sensitive material made available by the CI environment.
A malicious test introduced through a compromised dependency or source contribution could use an escape path to access those credentials and artifacts. If the attacker can obtain publishing credentials or modify a release artifact, the incident may affect downstream users. This is comparable to other build-pipeline compromises, such as the xz-utils compromise, although the entry point and mechanics differ.
Agent systems often run code on behalf of a user or service. The agent may receive API tokens, database credentials, cloud credentials, access to internal service endpoints, tool outputs, or customer data needed to complete a task. Model output or a prompt-injection payload can influence the code that the agent executes, so an escape from that code-execution environment can expose the same resources that the agent was allowed to use.
This does not make every agent framework equally exposed. The impact depends on the host runtime, its permissions, the tools enabled for the agent, and the credentials supplied to it. smolagents is one example in this research: its WASM executor ran under Deno with --allow-run, so code that reached the Deno host could start processes. The full case study appears below.
CI/CD jobs can expose publishing tokens, signing keys, source checkouts, and release artifacts. Agent systems can expose the credentials, tools, internal APIs, databases, cloud resources, and task data delegated to an agent. In both cases, the practical question is what the escaped host process can reach.
Node.js or Deno determines which host APIs escaped code can use. The environment around that runtime determines what those APIs lead to. A local development process with no secrets is a lower-value target than a CI release job or a production agent with cloud credentials and connectors.

Figure 4: The host runtime determines which operations an escape can perform, while the execution environment determines the credentials, data, and tools exposed to it.
This is not a fixed ranking. The risk depends on the permissions of the process, the tools enabled for it, its network access, the secrets it receives, and the data it can read or modify.
n8n's Code node allowed users to run Python to transform data between workflow steps. In the affected design, that code ran in Pyodide on Node.js with Python-level restrictions. The disclosed bypasses used ctypes and an internal Pyodide evaluation path to reach command execution as the n8n service process.
An attacker who could edit or create workflows could access the credentials and connected integrations available to that process, including OAuth tokens, database credentials, and API keys. The issue was assigned CVE-2025-68668 with a CVSS score of 9.9. n8n later moved Python execution to an external-runner model. Read the full N8Scape writeup.
Grist is an open-source relational spreadsheet whose server-side formula worker ran Python in Pyodide. It restricted direct JavaScript access through jsglobals, but the reported paths included Python class-hierarchy traversal and access to ctypes and Emscripten functions.
Those paths reached the Grist worker's host context and could expose documents, spreadsheet data, and other data or credentials available to that process. The issue was assigned CVE-2026-24002 with a CVSS score of 9.1. Grist addressed it in v1.7.9 by moving the sandbox to Deno with restricted permissions. Read the full Cellbreak writeup.
Cohere is an AI company that develops language models and tooling for building applications around them. Terrarium was its open-source Python execution sandbox for user- or LLM-generated code. It was intended for low-latency, containerized workloads such as data analysis, with support for common Pyodide packages including NumPy, pandas, and matplotlib.
Terrarium was maintained as an open-source project for roughly two years before Cohere archived it. Terrarium passed a restricted jsglobals object to Pyodide to limit which JavaScript objects sandboxed Python could access. That constrained the direct js module path, but ctypes remained available in the tested configuration. The following is the payload we used to demonstrate that path:
ctypes.CDLL(None) resolved Emscripten functions, and emscripten_run_script then ran JavaScript in the Node.js host context. The available Node APIs reflected the permissions of the container process. In the tested API configuration, submitted code did not require authentication.
We reported this ctypes-based escape to Cohere alongside a separate prototype-chain issue. Cohere fixed the prototype-chain issue as CVE-2026-5752 in Terrarium v1.0.1, but that change did not address the ctypes path described here. Cohere then archived the repository. With help from VulnCheck, this separate finding was assigned CVE-2026-61522; the CVE record lists Terrarium through v1.0.1 as affected and assigns a CVSS v3.1 score of 9.3 (CVSS v4.0: 9.4).
smolagents is Hugging Face's lightweight framework for building AI agents. Its CodeAgent pattern lets a model write Python and passes that Python to an executor for execution.
smolagents has a substantial public developer audience. As of July 2026, its GitHub repository had more than 28,000 stars, and PyPI reported roughly 586,000 downloads in the preceding month. These are not counts of unique users, but they show that the library has broad developer interest and active use.
Developers can give an agent a model and developer-defined tools, then use it for tasks such as data analysis, research, automation, or calculations. Python execution is useful in these workflows because the agent can use it to combine tool results, transform data, and implement task-specific logic. The default executor was local; executor_type="wasm" was an optional backend intended for lightweight local execution rather than the primary deployment mode.
When developers opted into the WASM executor, smolagents started a Deno subprocess, loaded Pyodide inside it, and sent the model-generated Python to that process. This was intended to keep generated Python separate from the main application process. Deno was the JavaScript host for Pyodide, and its permissions controlled what code could do after it reached that host.
The following payload reached the Deno context:
The default configuration granted Deno --allow-run so that agents could execute tools. As a result, code that reached the Deno context could spawn processes. HuggingFace removed the WasmExecutor in May 2026.
langchain-sandbox was a separate Pyodide-based sandbox project for code execution in LangChain-style applications. It was not the main LangChain package, and not every LangChain deployment used it.
Components like this are useful when an agent or application needs to run generated Python for calculations, data processing, or tool logic without placing that code directly in the application runtime. The intended benefit is a constrained execution environment for code that may be influenced by a model or user input.
langchain-sandbox embedded Pyodide in Deno and exposed configurable Deno permissions. That design was meant to give developers a place to restrict Python execution, but the tested implementation still exposed a ctypes path to system():
In the tested implementation, this call used Emscripten's syscall-emulation path rather than Deno's Command API. It therefore was not governed by Deno's allow_run setting. The maintainers archived the repository after disclosure.
Streamlit is a Python framework for turning scripts into interactive web applications, such as dashboards and data tools. stlite runs those applications through Pyodide/WASM instead of a conventional Python server: in a browser, embedded in a React application, through its online sharing editor, or as an Electron desktop application. stlite also powers Streamlit Preview, a VS Code extension with more than 93,000 Marketplace installs as of July 2026.
Developers use stlite to share prototypes, embed dashboards, and package Streamlit apps as offline .app or .exe applications. This is normally a distribution model for an application written by its developer, not a multi-tenant service that accepts arbitrary code from end users. The relevant attack scenario is therefore a malicious desktop app, or malicious code introduced into an app or its dependencies, being distributed to a user.
stlite runs the Streamlit Python application through Pyodide. By default, nodeJsWorker is false and the Python worker runs in Electron's sandboxed renderer. Setting nodeJsWorker: true moves Pyodide into a Node.js worker so the application can mount directories from the host filesystem. That optional setting changes the JavaScript APIs available after a Pyodide escape:
A malicious stlite application, or code introduced through a dependency, could execute with the permissions of the desktop application when a user runs it. The project characterized the risk as a deployer responsibility.
cibuildwheel is Python Packaging Authority tooling for building and testing Python wheels across operating systems and Python versions. Package maintainers add it to CI jobs such as GitHub Actions, GitLab CI, or CircleCI so the built wheel can be tested before it is uploaded. It is widely used release infrastructure: as of July 2026, PyPI had recorded more than 30 million cibuildwheel downloads, including roughly 750,000 in the preceding month.
Pyodide is an experimental cibuildwheel platform. When a maintainer opts into --platform pyodide, cibuildwheel cross-compiles the package, creates a pyodide venv, puts Node.js on the execution path, installs the built wheel, and runs the configured test command against copied test sources. Those tests run as Pyodide Python hosted by Node.js.
This becomes a supply-chain concern when unreviewed or compromised test code runs in a release job that contains credentials or artifacts. A malicious test can look like an ordinary regression check, or live in a helper that processes an innocuous-looking fixture such as a .dat file. Once the job runs that code, a Pyodide escape can reach the CI host and anything the job can access. The impact is similar to the xz-utils compromise: code that entered a trusted build path can affect downstream users. The route is different. The xz backdoor came from a long-term maintainer compromise and malicious build material; here, the payload is a test that escapes its Pyodide environment. A test can use the following pattern to reach that host:
Such code can be hidden in a test suite and can access environment variables and files available to the runner. Depending on the CI configuration, that may include GITHUB_TOKEN, PyPI publishing tokens, GPG keys, or release artifacts. The project also characterized the issue as a deployer responsibility.
n8n addressed the finding in October 2025 by moving execution to external runners and received CVE-2025-68668 (CVSS 9.9). In December, Grist moved the relevant worker from Node.js to Deno and received CVE-2026-24002 (9.1). smolagents removed its opt-in WasmExecutor in early 2026; that finding received CVE-2026-10613 (8.3).
The cohere-terrarium repository was archived after the final v1.0.1 security release. Our separate ctypes finding remained unpatched and received CVE-2026-61522 (9.3). The langchain-sandbox repository was also archived. stlite and cibuildwheel did not receive CVEs or ship code changes for these reports; both treated isolation as a deployer responsibility.
Those responses reflect different views of where the boundary belongs. A library can reasonably require deployers to provide process isolation when its documentation clearly states that requirement and its limitations. A product that presents an execution environment as isolated should likewise describe its threat model, host assumptions, and the controls needed for that claim to hold.
After the first two discoveries, we used the following checklist for initial triage:
Our GitHub and npm review identified additional repositories that use Pyodide with Python-level restrictions. We did not test each one. The presence of ctypes is a useful indicator, but exploitability still depends on the symbols available in the binary, the host runtime, and the permissions or data exposed to that host.
Reduce the exposed Python surface:
Reduce the Emscripten surface:
Constrain the host process:

Figure 5: Defense in depth for Pyodide deployments. Restrict the Python surface, export only required Emscripten functions, minimize host-runtime permissions, and isolate the process to protect files, secrets, and services.
Across the seven products we tested, Python-level restrictions did not account for the path from ctypes through Emscripten exports into the host runtime. The host runtime determines what an escape can call: in Node.js, it inherits the privileges of the service process; in Deno, it is limited by the permissions granted to that process. The execution environment determines what is at stake. A CI runner may provide release credentials and build artifacts, while an agentic application may provide access to tools and delegated credentials.
For researchers, loadPyodide and related integration code are useful starting points. Establishing an exploit requires more than importing ctypes: the investigation should also verify the exported symbols, host runtime, permissions, and accessible data.
For product teams, the central design question is where enforcement occurs. Import controls can reduce exposure, but a deployment that runs untrusted code also needs to limit the Emscripten exports and host-process capabilities that remain reachable after an interpreter-level control fails.
This research will be presented at DEF CON 34 on Friday, August 7, 2026, at 14:00 Las Vegas time.
Published research:
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。