From 7fa49bd5506eda1c296e55dc8349994f4527cb03 Mon Sep 17 00:00:00 2001 From: John Kennedy <65985482+jkennedyvz@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:05:55 -0700 Subject: [PATCH] docs: document LANGGRAPH_STRICT_MSGPACK for checkpoint security (#7517) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Add `LANGGRAPH_STRICT_MSGPACK=true` guidance to `JsonPlusSerializer` docstring and inline comments - Update the warning message emitted for unregistered types to mention the env var - Add module docstring to `_msgpack.py` explaining the safety controls - Add Security sections to checkpoint, checkpoint-postgres, and checkpoint-sqlite READMEs ## Context Multiple security advisories have reported the same msgpack deserialization pattern (`ext_hook` → `importlib.import_module` → `getattr` → call). The underlying behavior is documented in the repo's threat model as T1, but the `LANGGRAPH_STRICT_MSGPACK` env var that mitigates it is not surfaced in user-facing docs, docstrings, or warning messages. This PR closes that gap. ## Test plan - [x] Verify READMEs render correctly on GitHub (callout boxes use `> [!IMPORTANT]` syntax) - [x] Verify `JsonPlusSerializer` docstring renders in IDE tooltips - [x] Confirm warning message format: `LANGGRAPH_STRICT_MSGPACK=true PYTHON_CMD 2>&1 | grep -i strict` --------- Co-authored-by: Claude Opus 4.6 (1M context) --- libs/checkpoint-postgres/README.md | 5 +++++ libs/checkpoint-sqlite/README.md | 5 +++++ libs/checkpoint/README.md | 3 +++ libs/checkpoint/langgraph/checkpoint/serde/_msgpack.py | 7 +++++++ libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py | 10 +++++++++- 5 files changed, 29 insertions(+), 1 deletion(-) diff --git a/libs/checkpoint-postgres/README.md b/libs/checkpoint-postgres/README.md index c36a60c60..320ecf839 100644 --- a/libs/checkpoint-postgres/README.md +++ b/libs/checkpoint-postgres/README.md @@ -6,6 +6,11 @@ Implementation of LangGraph CheckpointSaver that uses Postgres. By default `langgraph-checkpoint-postgres` installs `psycopg` (Psycopg 3) without any extras. However, you can choose a specific installation that best suits your needs [here](https://www.psycopg.org/psycopg3/docs/basic/install.html) (for example, `psycopg[binary]`). +## Security + +> [!IMPORTANT] +> Set `LANGGRAPH_STRICT_MSGPACK=true` or pass an explicit `allowed_msgpack_modules` list when creating your checkpointer. This restricts checkpoint deserialization to known-safe types, preventing code execution if the database is compromised. See the [langgraph-checkpoint README](https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint#serde) for details. + ## Usage > [!IMPORTANT] diff --git a/libs/checkpoint-sqlite/README.md b/libs/checkpoint-sqlite/README.md index 48f4e6690..f38f0c311 100644 --- a/libs/checkpoint-sqlite/README.md +++ b/libs/checkpoint-sqlite/README.md @@ -2,6 +2,11 @@ Implementation of LangGraph CheckpointSaver that uses SQLite DB (both sync and async, via `aiosqlite`) +## Security + +> [!IMPORTANT] +> Set `LANGGRAPH_STRICT_MSGPACK=true` or pass an explicit `allowed_msgpack_modules` list when creating your checkpointer. This restricts checkpoint deserialization to known-safe types, preventing code execution if the database is compromised. See the [langgraph-checkpoint README](https://github.com/langchain-ai/langgraph/tree/main/libs/checkpoint#serde) for details. + ## Usage ```python diff --git a/libs/checkpoint/README.md b/libs/checkpoint/README.md index 4152c3824..3bfcbccaf 100644 --- a/libs/checkpoint/README.md +++ b/libs/checkpoint/README.md @@ -26,6 +26,9 @@ You must pass these when invoking the graph as part of the configurable part of `langgraph_checkpoint` also defines protocol for serialization/deserialization (serde) and provides an default implementation (`langgraph.checkpoint.serde.jsonplus.JsonPlusSerializer`) that handles a wide variety of types, including LangChain and LangGraph primitives, datetimes, enums and more. +> [!IMPORTANT] +> **Checkpoint deserialization security:** By default the serializer allows any Python type found in checkpoint data. New applications should set the environment variable `LANGGRAPH_STRICT_MSGPACK=true` or pass an explicit `allowed_msgpack_modules` list to `JsonPlusSerializer` to restrict deserialization to known-safe types. + ### Pending writes When a graph node fails mid-execution at a given superstep, LangGraph stores pending checkpoint writes from any other nodes that completed successfully at that superstep, so that whenever we resume graph execution from that superstep we don't re-run the successful nodes. diff --git a/libs/checkpoint/langgraph/checkpoint/serde/_msgpack.py b/libs/checkpoint/langgraph/checkpoint/serde/_msgpack.py index 0b09a9a4e..35ac48665 100644 --- a/libs/checkpoint/langgraph/checkpoint/serde/_msgpack.py +++ b/libs/checkpoint/langgraph/checkpoint/serde/_msgpack.py @@ -1,3 +1,10 @@ +"""Msgpack deserialization safety controls. + +Set ``LANGGRAPH_STRICT_MSGPACK=true`` to restrict checkpoint deserialization +to the types listed in ``SAFE_MSGPACK_TYPES``. Without this, any Python +callable stored in checkpoint data will be imported and executed on load. +""" + import os from collections.abc import Iterable from typing import cast diff --git a/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py b/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py index 4ef4a40b2..4ee654df8 100644 --- a/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py +++ b/libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py @@ -56,6 +56,10 @@ class JsonPlusSerializer(SerializerProtocol): class and called within the Pregel loop. It should not be used on untrusted python objects. If an attacker can write directly to your checkpoint database, they may be able to trigger code execution when data is deserialized. + + Set the environment variable ``LANGGRAPH_STRICT_MSGPACK=true`` to restrict + deserialization to a built-in allowlist of safe types. You can also pass + an explicit ``allowed_msgpack_modules`` to the constructor. """ def __init__( @@ -70,8 +74,11 @@ class JsonPlusSerializer(SerializerProtocol): ) -> None: if allowed_msgpack_modules is _lg_msgpack._SENTINEL: if _lg_msgpack.STRICT_MSGPACK_ENABLED: + # Strict: only SAFE_MSGPACK_TYPES are allowed. allowed_msgpack_modules = None else: + # Permissive (default): all types allowed with a warning. + # Set LANGGRAPH_STRICT_MSGPACK=true to lock this down. allowed_msgpack_modules = True self.pickle_fallback = pickle_fallback self._allowed_json_modules: set[tuple[str, ...]] | Literal[True] | None = ( @@ -530,7 +537,8 @@ def _create_msgpack_ext_hook( logger.warning( "Deserializing unregistered type %s.%s from checkpoint. " "This will be blocked in a future version. " - "Add to allowed_msgpack_modules to silence: [(%r, %r)]", + "Set LANGGRAPH_STRICT_MSGPACK=true to block now, or add " + "to allowed_msgpack_modules to allow explicitly: [(%r, %r)]", module, name, module,