-
Notifications
You must be signed in to change notification settings - Fork 276
Description
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