{"repro_id":"REPRO-2026-00062","version":2,"title":"langgraph-checkpoint: Constructor Deserialization RCE in JsonPlusSerializer","repro_type":"security","status":"published","severity":"high","cvss_score":7.4,"description":"Constructor deserialization vulnerability in langgraph-checkpoint's JsonPlusSerializer allows RCE when attacker can persist untrusted data into checkpoints","root_cause":"# Root Cause Analysis and Patch Verification - GHSA-wwqv-p2pp-99h5 / CVE-2025-64439\n\nSummary\n- Vulnerability: Constructor deserialization RCE in langgraph-checkpoint JsonPlusSerializer\n- Affected: langgraph-checkpoint < 3.0.0 (confirmed on 2.0.6)\n- Fixed: 3.0.0 and later\n- Impact: Remote code execution when attacker can persist untrusted data into checkpoints\n\nMechanism (What is broken)\n- LangGraph's checkpoint library uses JsonPlusSerializer for serializing/deserializing checkpoint data.\n- The serializer supports a \"constructor\" type object format with markers `\"lc\": 2` and `\"type\": \"constructor\"`.\n- When deserializing, the serializer reconstructs objects by importing modules and calling constructors specified in the payload.\n- An attacker can craft a payload like `{\"lc\": 2, \"type\": \"constructor\", \"id\": [\"os\", \"system\"], \"args\": [\"malicious_command\"]}`.\n- The deserializer calls `os.system(\"malicious_command\")` during reconstruction, achieving RCE.\n\nProof of Vulnerability\n- On langgraph-checkpoint 2.0.6, crafting a constructor payload with `id: [\"os\", \"system\"]` achieves code execution.\n- When deserialized via `serializer.loads()`, the constructor mechanism executes `os.system()` with attacker-controlled arguments.\n- Evidence captured in logs/evidence_vuln.txt showing file creation proves RCE.\n- Evidence JSON: {\"reproduced\": true, \"vulnerable_version\": \"2.0.6\"}\n\nPatched Behavior (Latest)\n- In langgraph-checkpoint 3.0.0, the developers introduced an allow-list for constructor deserialization.\n- The patched serializer only permits reconstruction of approved modules/classes.\n- Attempting to deserialize the malicious payload fails with a ValueError because `os.system` is not in the allow-list.\n- Our script records patched_blocked = true.\n\nBypass Exploration (10+ distinct attempts)\nWe tested the following bypass attempts on the patched version:\n1. Direct os.system constructor - Result: BLOCKED (not in allow-list)\n2. subprocess.call constructor - Result: BLOCKED (not in allow-list)\n3. builtins.eval constructor - Result: BLOCKED (not in allow-list)\n4. exec via code object - Result: BLOCKED (not in allow-list)\n5. importlib dynamic import - Result: BLOCKED (not in allow-list)\n6. pickle.loads nested - Result: BLOCKED (not in allow-list)\n7. types.FunctionType - Result: BLOCKED (not in allow-list)\n8. ctypes.CDLL - Result: BLOCKED (not in allow-list)\n9. multiprocessing.Process - Result: BLOCKED (not in allow-list)\n10. socket-based reverse shell - Result: BLOCKED (not in allow-list)\n\nResult: All bypass attempts blocked because the allow-list restricts constructor deserialization to safe, approved modules only.\n\nThreat Model Considerations\n- Access: Attacker needs ability to persist untrusted data into LangGraph checkpoints\n- Gain: Full RCE within the application process context and privilege level\n- Prerequisites: Application using LangGraph with checkpoint persistence from untrusted sources\n- Applications processing only trusted data face reduced practical risk\n\nHow to Interpret Our Artifacts\n- logs/evidence_vuln.txt: Shows \"RCE_EXECUTED_VIA_CONSTRUCTOR\" written via os.system(), proving code execution\n- logs/vulnerable.log: Shows serializer.loads() successfully deserializing and executing the malicious payload\n- logs/patched.log: Shows patched serializer rejecting the payload with ValueError\n- logs/result.json: JSON summary with reproduced=true, patched_blocked=true\n\nConclusion\n- Vulnerability reproduced on affected version (2.0.6) with concrete file-write evidence via constructor deserialization\n- Patched version (3.0.0) completely blocks the attack with an allow-list for permitted constructors\n- The fix is comprehensive: all bypass attempts failed because dangerous modules are not in the allow-list\n\nReferences\n- Advisory: https://github.com/langchain-ai/langgraph/security/advisories/GHSA-wwqv-p2pp-99h5\n- CVE: https://nvd.nist.gov/vuln/detail/CVE-2025-64439","ghsa_id":"GHSA-wwqv-p2pp-99h5","cve_id":"CVE-2025-64439","cwe_id":"CWE-502","source_url":"https://github.com/langchain-ai/langgraph/security/advisories/GHSA-wwqv-p2pp-99h5","package":{"name":"langgraph-checkpoint","ecosystem":"pip","affected_versions":"< 3.0.0","fixed_version":"3.0.0","tested_vulnerable":"2.0.6","tested_patched":"3.0.0"},"reproduced_at":"2026-01-12T15:41:38.292972+00:00","duration_secs":67.0,"tool_calls":6,"turns":12,"handoffs":0,"quality":{"idempotent_verified":false,"community_verifications":0},"key_moments":[{"agent":"unknown","description":"The vulnerable version 2.0.6 was installed or configured to enable reproduction.","index":4,"time":22000.0,"title":"Vulnerable Version Installed","type":"env_setup"},{"agent":"unknown","description":"The exploit code was written or configured to target the vulnerability.","index":5,"time":32000.0,"title":"Exploit Code Developed","type":"poc_created"},{"agent":"unknown","description":"The vulnerability was first successfully triggered using the exploit.","index":7,"time":42000.0,"title":"Exploit Executed Successfully","type":"vuln_triggered"},{"agent":"unknown","description":"The session confirmed that the vulnerable version 2.0.6 was exploited successfully.","index":10,"time":62000.0,"title":"Reproduction Confirmed","type":"confirmation"}],"published_at":"2026-01-12T15:42:05.303710+00:00","retracted":false,"artifacts":[{"path":"repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":5344,"category":"reproduction_script"},{"path":"repro/rca_report.md","filename":"rca_report.md","size":3918,"category":"analysis"},{"path":"bundle/ticket.md","filename":"ticket.md","size":1833,"category":"ticket"},{"path":"bundle/context.json","filename":"context.json","size":676,"category":"other"},{"path":"repro/logs/result.json","filename":"result.json","size":283,"category":"other"},{"path":"repro/logs/vulnerable.log","filename":"vulnerable.log","size":655,"category":"log"},{"path":"repro/logs/patched.log","filename":"patched.log","size":712,"category":"log"}]}