Skip to content

ArrayBuffer constructor bypasses disable_pyimport() sandbox via bytearray → PyObjectWrapper escape #340

Description

@athuljayaram

Summary

A sandbox escape vulnerability exists in js2py v0.74 (latest) that bypasses the protection offered by js2py.disable_pyimport(). By calling new ArrayBuffer(n) from JavaScript, an
attacker can obtain a PyObjectWrapper wrapping a Python bytearray object, then traverse the Python class hierarchy to reach subprocess.Popen and execute arbitrary OS commands.

This is a bypass of the fix proposed in PR #323 (CVE-2024-28397). Even if that patch is applied, this vector is unaffected since it originates in jsarraybuffer.py, not jsobject.py.


Affected Version

  • js2py ≤ 0.74 (all released versions)

Root Cause

File: js2py/constructors/jsarraybuffer.py

@js
def ArrayBuffer():
a = arguments[0]
if isinstance(a, PyJsNumber):
length = a.to_uint32()
if length != a.value:
raise MakeError('RangeError', 'Invalid array length')
temp = Js(bytearray([0] * length)) # ← bytearray passed to Js()
return temp
return Js(bytearray([0])) # ← same issue

File: js2py/base.py — py_wrap() function

def py_wrap(py):
if isinstance(py, (FunctionType, BuiltinFunctionType, MethodType,
BuiltinMethodType, dict, int, str, bool, float, list,
tuple, long, basestring)) or py is None:
return HJs(py)
return PyObjectWrapper(py) # ← bytearray is NOT in the allowlist → lands here

bytearray is not included in py_wrap()'s safe type allowlist. When Js(bytearray(...)) is called, it falls through to py_wrap(), which wraps the raw Python bytearray in a
PyObjectWrapper. This class exposes unrestricted getattr() access to the underlying Python object, allowing full traversal of the Python object hierarchy.


Relationship to CVE-2024-28397

CVE-2024-28397 used Object.getOwnPropertyNames({}) to leak a dict_keys view object into PyObjectWrapper. PR #323 fixes this by changing:

jsobject.py

return obj.own.keys() # before (returns dict_keys → PyObjectWrapper)
return list(obj.own.keys()) # after (returns list → safe)

This fix addresses only jsobject.py. The jsarraybuffer.py path is entirely separate and remains exploitable regardless of whether PR #323 is applied.


Impact

  • Full sandbox escape from the js2py JavaScript environment
  • Arbitrary OS command execution on the host system
  • Bypasses js2py.disable_pyimport() entirely
  • Affects any application that passes untrusted JavaScript to js2py.eval_js() or js2py.eval_js6()

Suggested Fix

Option 1 (minimal): Add bytearray and bytes to py_wrap()'s allowlist in base.py:

def py_wrap(py):
if isinstance(py, (FunctionType, BuiltinFunctionType, MethodType,
BuiltinMethodType, dict, int, str, bool, float, list,
tuple, long, basestring,
bytearray, bytes)): # ← add these
return HJs(py)
return PyObjectWrapper(py)

Option 2 (recommended): Audit the full py_wrap() allowlist for all Python types that could become PyObjectWrapper. Other non-listed types include dict_values, dict_items, set,
frozenset, range, map, filter, zip, and generator objects — any of which could be a future vector.

Option 3 (defense in depth): Make PyObjectWrapper.get() block access to dunder attributes (class, base, subclasses, etc.) as a secondary hardening measure.

Reference

https://securityinfinity.com/research/beyond-the-patch-a-new-sandbox-escape-in-js2py-via-arraybuffer

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions