Description: When
LANGGRAPH_STRICT_MSGPACK is not set (the default), the
msgpack _create_msgpack_ext_hook allows
any(module, class) pair stored in
checkpoint data to be imported via importlib.import_module
and instantiated with attacker-controlled arguments. The
SAFE_MSGPACK_TYPES allowlist (47 entries) is checked first,
but unregistered types are logged as warnings and allowed through when
allowed_modules=True (the default when strict mode is off).
Seven EXT codes are processed: EXT_CONSTRUCTOR_SINGLE_ARG
(0), EXT_CONSTRUCTOR_POS_ARGS (1),
EXT_CONSTRUCTOR_KW_ARGS (2),
EXT_METHOD_SINGLE_ARG (3), EXT_PYDANTIC_V1
(4), EXT_PYDANTIC_V2 (5), EXT_NUMPY_ARRAY (6).
The BaseCache component uses
JsonPlusSerializer(pickle_fallback=False) but inherits the
same msgpack allowed_modules default behavior. The proposed
RestrictedUnpickler (secure_pickle.py)
documented in SECURITY_FIX_SUMMARY.md was never merged —
pickle remains unrestricted when enabled.
Preconditions: Attacker must have write access to
the checkpoint database (PostgreSQL or SQLite). This requires
compromised database credentials or a co-located attacker.
Description: When pickle_fallback=True
is explicitly passed to JsonPlusSerializer, checkpoint data
with type tag "pickle" is deserialized via
pickle.loads() with zero restrictions
(libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py:JsonPlusSerializer.loads_typed).
Preconditions: (1) Application or checkpointer
explicitly enables pickle_fallback=True. (2) Attacker
writes ("pickle", <payload>) to checkpoint
storage.
Description: The JSON _reviver handles
lc:2 type constructors by importing the module path from
checkpoint JSON data via importlib.import_module
(libs/checkpoint/langgraph/checkpoint/serde/jsonplus.py:JsonPlusSerializer._revive_lc2).
If allowed_json_modules=True (explicit opt-in), any module
reachable in the Python environment can be imported and instantiated.
The method also supports method chaining — a method key in
the JSON can call arbitrary methods on the imported class.
Preconditions: (1)
allowed_json_modules set to True (not the
default). (2) Attacker writes crafted JSON to checkpoint storage.
T4: RemoteGraph
Unvalidated Inbound Data
Flow: DF5 (RemoteGraph -> Pregel)
Description: Stream events from the remote
LangGraph Server are deserialized from JSON and dict-splatted into
Interrupt(**i) and Command(**chunk.data)
without schema validation
(libs/langgraph/langgraph/pregel/remote.py:RemoteGraph.stream).
A compromised or malicious remote server can inject unexpected fields.
Command.update can carry arbitrary state modifications;
Command.goto can alter graph execution flow.
Interrupt accepts **deprecated_kwargs which
includes a ns parameter that can override interrupt ID
generation via xxh3_128_hexdigest.
Preconditions: User connects
RemoteGraph to a compromised or attacker-controlled server
URL.
Description: Config values from
langgraph.json are serialized via json.dumps()
and embedded in single-quoted ENV directives across
multiple config sections (store, auth, encryption, http, webhooks,
checkpointer, ui, ui_config, graphs). JSON does not escape single
quotes, so a config value containing ' could break the
Dockerfile syntax or inject additional Dockerfile instructions. The
pattern is duplicated in two Dockerfile generation functions
(libs/cli/langgraph_cli/config.py:python_config_to_docker
and
libs/cli/langgraph_cli/config.py:node_config_to_docker).
Preconditions: A langgraph.json config
value contains a single quote character.
T6: ZIP Slip in Template
Extraction
Flow: DF7 (CLI template download)
Description: langgraph new downloads a
ZIP from GitHub and uses zip_file.extractall(path). If the
archive contains path-traversal entries (e.g.,
../../etc/cron.d/exploit), files could be written outside
the target directory.
Preconditions: The GitHub-hosted template archive
must contain malicious path entries. This requires compromise of the
upstream template repo.
T7: AES Key Entropy
via Environment Variable
Flow: DF10 (User config ->
EncryptedSerializer)
Description: The AES key is loaded from
LANGGRAPH_AES_KEY as a UTF-8 string and
.encode()d to bytes
(libs/checkpoint/langgraph/checkpoint/serde/encrypted.py:EncryptedSerializer.from_pycryptodome_aes).
This limits key entropy to printable characters (~6.57 bits/byte vs. 8
bits/byte for random bytes), reducing effective key strength for AES-128
from 128 bits to ~105 bits.
Preconditions: User relies on environment variable
path for key loading (vs. passing raw bytes directly via
key= parameter).
T8: EncryptedSerializer
Assert Bypass
Flow: DF10 (Encrypted checkpoint data)
Description: The cipher name check in
decrypt() uses assert ciphername == "aes"
(libs/checkpoint/langgraph/checkpoint/serde/encrypted.py:PycryptodomeAesCipher.decrypt),
which is stripped when Python runs with -O (optimize) flag.
The ciphername value comes from the type tag in checkpoint
storage (split from the type+cipher format).
Preconditions: Python running with -O
flag AND attacker can write to checkpoint storage.
T9:
SDK API Key Leak via Server-Controlled Location Redirect
Description: The SDK’s
HttpClient.request_reconnect()
(libs/sdk-py/langgraph_sdk/_async/http.py:HttpClient.request_reconnect)
follows server-provided Location headers and forwards the
full request_headers dict (including the
x-api-key authentication header) to the redirected URL. The
HttpClient.stream() method
(libs/sdk-py/langgraph_sdk/_async/http.py:HttpClient.stream)
also follows Location headers for SSE reconnection and
forwards reconnect_headers (which include
x-api-key) to the server-controlled URL. No URL validation,
same-origin check, or sensitive header stripping is performed before
following the redirect. The same pattern exists in the sync client
(libs/sdk-py/langgraph_sdk/_sync/http.py).
Preconditions: (1) User connects SDK to a LangGraph
Server that is compromised or attacker-controlled. (2) The server
returns a response with a Location header pointing to an
attacker-controlled URL.
T10:
EncryptedSerializer Encryption Bypass via Unencrypted Data
Injection
Description:
EncryptedSerializer.loads_typed()
(libs/checkpoint/langgraph/checkpoint/serde/encrypted.py:EncryptedSerializer.loads_typed)
checks if the type tag contains a + delimiter. If it does
not (e.g., type tag is "msgpack" instead of
"msgpack+aes"), the data is passed directly to the inner
serde’s loads_typed()without any decryption or MAC
verification. An attacker with write access to checkpoint
storage can bypass the encryption layer entirely by writing data with a
plain type tag.
Preconditions: (1) Application uses
EncryptedSerializer for checkpoint protection. (2) Attacker
has write access to checkpoint storage.
T11: Unbounded
Checkpoint Data Retention
Flow: DF1, DF11 (Checkpoint Storage lifecycle)
Description: Checkpoint data (DC3, DC6) is retained
indefinitely by default. No built-in TTL, pruning, or data lifecycle
management in the library-level checkpoint savers. Conversation history
containing user PII may accumulate without bounds.
Preconditions: Application uses checkpointing (the
primary use case). No explicit cleanup configured.