Cybersecurity News

Beyond the Patch: A New Sandbox Escape in js2py via ArrayBuffer

Beyond the Patch: A New Sandbox Escape in js2py via ArrayBuffer

JavaScript execution within Python applications has become a routine part of modern software development. Libraries like js2py make it possible to evaluate JavaScript code directly inside a Python runtime, enabling a wide range of use cases from web scraping to API automation. But running untrusted code in any environment demands strong isolation, and as the security community continues to learn, sandbox implementations often fall short of their promises.

In mid-2024, CVE-2024-28397 demonstrated that js2py's primary sandbox protection — the disable_pyimport() function — could be bypassed entirely to achieve arbitrary code execution on the host system. A fix was proposed by security researcher Marven11 in pull request #323, reviewed and approved by the project maintainer, and then quietly left unmerged. As of February 2026, js2py version 0.74, released in November 2022, remains the latest version available on PyPI. No official patch has shipped.

During independent research into the unpatched state of the library, a new and distinct sandbox escape was discovered. By invoking JavaScript's built-in ArrayBuffer constructor, an attacker can escape the js2py sandbox and reach arbitrary Python code execution — even with disable_pyimport() enabled and even after manually applying the PR #323 patch. The root cause is not a single overlooked line but an architectural weakness that the original fix never fully addressed.

What Is js2py?

js2py is a pure Python library designed to translate and execute JavaScript code within a Python interpreter. Unlike solutions that embed an external JavaScript engine such as V8, js2py reimplements JavaScript semantics entirely in Python, including the type system, prototype chain, scope resolution, and built-in objects. The library requires no native compilation, no external dependencies beyond Python itself, and runs on any platform that supports Python.

This simplicity has made js2py attractive to a generation of Python developers who needed lightweight JavaScript evaluation without the overhead of spawning Node.js subprocesses or compiling native extensions. Projects like pyload, cloudscraper, and lightnovel-crawler integrated js2py to handle JavaScript encountered during web interactions — Cloudflare challenge scripts, obfuscated API signatures, or dynamically generated download tokens.

The library exposes a straightforward API. Calling js2py.eval_js() evaluates a JavaScript expression and returns the result to Python. Calling js2py.disable_pyimport() before evaluation is documented as a security measure that prevents JavaScript code from importing Python modules, theoretically limiting what an attacker could do if they controlled the JavaScript input.

In practice, this protection has proven to be superficial. The underlying bridge between the JavaScript and Python type systems creates opportunities for sandbox escape that cannot be closed by simply disabling Python imports.

The Architecture of the Bridge

To understand both the original vulnerability and the newly discovered bypass, it is necessary to understand how js2py moves values between the JavaScript and Python worlds. The central mechanism is a function called py_wrap(), located in js2py/base.py. This function takes a Python object and returns an appropriate JavaScript-compatible wrapper.

For common Python types, py_wrap() routes values through a safe conversion path. Strings, integers, floats, booleans, lists, tuples, dicts, and several function types are recognized and converted into proper JavaScript primitives or objects. The conversion is well-defined and does not expose the underlying Python object to the JavaScript environment.

For any type not on this explicit allowlist, py_wrap() falls back to returning a PyObjectWrapper instance. This class wraps the raw Python object and exposes it to JavaScript through an unrestricted getattr() interface. Every attribute access from JavaScript on a PyObjectWrapper calls Python's built-in getattr() on the underlying object, with no filtering or restrictions of any kind.

This is the root cause of every js2py sandbox escape discovered to date. Once an attacker obtains a PyObjectWrapper around any Python object, they can traverse the Python class hierarchy: accessing __class__, then __base__, then calling __subclasses__() to enumerate every Python class loaded in the interpreter. From that list, they can locate and instantiate powerful classes to execute arbitrary system commands, regardless of what restrictions disable_pyimport() imposes.

CVE-2024-28397: The Original Escape

The original sandbox escape, disclosed publicly as CVE-2024-28397 in June 2024, exploited a Python 2 to Python 3 compatibility oversight in the implementation of JavaScript's Object.getOwnPropertyNames() function, found in js2py/constructors/jsobject.py.

The function returned the result of calling .keys() on an internal Python dictionary. In Python 2, dict.keys() returns a plain list. In Python 3, the same call returns a dict_keys view object instead. The dict_keys type is not included in py_wrap()'s safe type allowlist, so it was wrapped in a PyObjectWrapper and returned to the JavaScript caller.

From that point, any JavaScript code executing inside js2py could use the standard Python class traversal technique to reach powerful execution primitives. The disable_pyimport() call provided no protection because the escape did not involve importing any Python module — it used only attribute access on an already-exposed Python object.

Researcher Marven11 submitted a fix in pull request #323 in March 2024. The change was minimal: convert the return value from a raw dict_keys view to a plain list by wrapping it in list(). By doing so, the return value becomes a type that py_wrap() recognizes as safe, and the dict_keys object never reaches PyObjectWrapper. The fix was approved by the project maintainer but was never included in a release.

An Incomplete Fix and an Open Door

The proposed fix in PR #323, while technically correct for its specific target, addressed only the symptom in one function. The structural weakness — the incomplete py_wrap() allowlist — was left entirely unchanged. The same class of vulnerability exists wherever js2py code returns a Python type not included in that allowlist to the JavaScript environment.

The py_wrap() allowlist covers: function types, built-in function types, method types, dicts, integers, strings, booleans, floats, lists, tuples, and the Python 2 legacy types long and basestring. Many common Python types are absent from this list. Among them: bytearray, bytes, dict_values, dict_items, set, frozenset, range, map objects, filter objects, zip objects, and generators. Any of these types, if returned from a js2py internal function, would fall through to PyObjectWrapper.

The question, then, is not whether additional escape paths exist. It is simply which code paths produce these unhandled types and how accessible they are from JavaScript.

The New Bypass: ArrayBuffer

The newly discovered bypass originates in js2py/constructors/jsarraybuffer.py, which implements JavaScript's ArrayBuffer constructor — a standard part of the JavaScript language used to represent fixed-length raw binary data buffers.

When JavaScript code calls new ArrayBuffer(n), the js2py implementation creates a Python bytearray of length n and passes it to the Js() type conversion function to produce a JavaScript-compatible value. This is where the vulnerability materializes. The bytearray type is not present in py_wrap()'s safe type allowlist. When Js() receives a bytearray, it finds no matching conversion case and delegates to py_wrap(), which wraps the raw Python bytearray object in a PyObjectWrapper.

The JavaScript caller receives what appears to be a JavaScript object but is in fact a PyObjectWrapper backed by a live Python bytearray. Through this wrapper, every Python attribute of the bytearray — and through class traversal, every attribute of every Python object reachable from it — is accessible from JavaScript with no restrictions. The escape from there follows the same technique used in CVE-2024-28397: traverse the class hierarchy to find execution primitives and run arbitrary OS commands.

This bypass works on js2py 0.74 as installed via pip. It also works on a version with PR #323 manually applied, because that patch does not touch jsarraybuffer.py or py_wrap(). Applying the fix for CVE-2024-28397 has no effect whatsoever on the ArrayBuffer escape path. These are entirely parallel vulnerabilities sharing a common architectural cause.

Impact and Affected Projects

The practical impact of this new bypass is equivalent to CVE-2024-28397: a full sandbox escape enabling arbitrary OS command execution from within the js2py JavaScript environment, with disable_pyimport() providing no protection.

Any application that evaluates JavaScript from an untrusted or attacker-influenced source using js2py is exposed. This includes applications where the JavaScript is fetched from a remote server, derived from user input, or received as part of an HTTP API response. The attacker does not need prior access to the Python environment — control over the JavaScript input is sufficient.

The downstream projects most visibly at risk are those identified in the original CVE disclosure. pyload, a download management application, uses js2py to evaluate JavaScript encountered during link resolution workflows. A malicious hosting server could serve crafted JavaScript to achieve code execution on the pyload host. cloudscraper uses js2py as a fallback JavaScript interpreter for handling browser challenge scripts. Any website that a cloudscraper-based tool visits could theoretically deliver a malicious challenge response and achieve code execution on the client. lightnovel-crawler similarly executes JavaScript scraped from novel hosting sites, where operator-injected or intercepted JavaScript could trigger the bypass.

Given that these libraries are themselves dependencies of many other Python packages and applications, the exposure extends far beyond the directly named projects.

No official patched release of js2py exists as of February 2026. Developers with a dependency on js2py should assess their exposure and consider the following actions.

The most targeted mitigation for this specific issue is to add bytearray and bytes to the safe type allowlist inside py_wrap() in base.py. This closes the ArrayBuffer escape while leaving the broader codebase unchanged. A more thorough approach would involve auditing the complete allowlist against every Python built-in type that could plausibly be produced inside js2py and adding all of them — including set, frozenset, range, dict_values, dict_items, and iterator types. Neither approach fixes the underlying design, but both raise the cost of exploitation.

A complementary hardening layer would be to restrict attribute access within PyObjectWrapper.get() to block Python dunder attributes such as __class__, __base__, and __subclasses__. This would not prevent PyObjectWrapper from being created through unhandled types, but it would break the standard traversal technique used to reach execution primitives, forcing an attacker to find an alternative escalation path.

For applications where security is a genuine concern, migrating away from js2py is the most reliable recommendation. Running JavaScript in a Node.js subprocess with restricted OS permissions, using a containerized execution environment with seccomp filtering, or eliminating the JavaScript execution requirement entirely are all preferable to relying on a pure-Python sandbox from an unmaintained library. No pure-Python JavaScript execution environment has demonstrated the ability to safely isolate fully untrusted JavaScript, and js2py's development activity since 2022 makes future improvements unlikely.

Responsible Disclosure

This vulnerability was identified independently during research into the unpatched state of CVE-2024-28397. The finding has been reported to the js2py project and a CVE identifier is being requested through MITRE. The decision to publish follows standard responsible disclosure timelines and takes into account the effectively inactive state of the upstream project, the absence of any patch for the original CVE after nearly two years, and the need for affected downstream projects to have the information required to protect their users.

Downstream maintainers of projects that depend on js2py are encouraged to evaluate their exposure, apply the targeted mitigations described above where feasible, and communicate the risk to their own users if js2py is used in a context involving untrusted JavaScript input.

Conclusion

The discovery of a new sandbox escape in js2py via the ArrayBuffer constructor is a predictable consequence of an incomplete fix. CVE-2024-28397 was treated as a single-function bug when it was in fact a symptom of a broader architectural problem: the py_wrap() type allowlist is incomplete, and PyObjectWrapper exposes raw Python objects to JavaScript with no attribute filtering. Fixing one function that produced one unhandled type did not, and could not, close the vulnerability class.

For the security research community, js2py remains a productive area of study. Multiple Python types outside the py_wrap() allowlist exist throughout the codebase, and the ArrayBuffer path is unlikely to be the last one identified. For developers currently depending on js2py, this research reinforces a clear message: disable_pyimport() is not a security boundary, and js2py should not be used to evaluate untrusted JavaScript in any production environment without additional isolation layers.

The library has not seen a commit since November 2022. An approved security fix sits in an open pull request with no path to release. Until js2py is either actively maintained again or officially succeeded by a secure alternative, the attack surface will continue to grow as researchers find the next unhandled type in the next unaudited file.