Skip to content

ArrayBuffer constructor bypasses disable_pyimport() sandbox via bytearray β†’ PyObjectWrapper escapeΒ #340

@athuljayaram

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions