VAITP Dataset

← Back to the dataset

CVE-2026-44843

LangChain insecure deserialization allows instantiation with untrusted arguments.

  • CVSS 8.2
  • CWE-502
  • Input Validation and Sanitization
  • Remote

LangChain is a framework for building agents and LLM-powered applications. Prior to 0.3.85 and 1.3.3, LangChain contains older runtime code paths that deserialize run inputs, run outputs, or other application-controlled payloads using overly broad object allowlists. These paths may call load() with allowed_objects="all". This does not enable arbitrary Python object deserialization, but it does allow any trusted LangChain-serializable object to be revived, which is broader than these runtime paths require. As a result, attacker-supplied LangChain serialized constructor dictionaries may cause trusted runtime paths to instantiate classes with untrusted constructor arguments. This vulnerability is fixed in 0.3.85 and 1.3.3.

CVSS base score
8.2
Published
2026-05-26
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
Remote
Impact
Arbitrary Code Execution
Affected component
LangChain
Fixed by upgrading
Yes

Solution

Upgrade to LangChain version 0.3.85 or 1.3.3.

Vulnerable code sample

import json
import os
import sys

# This example simulates the vulnerability in a self-contained manner.
# It represents the core issue: a deserialization mechanism that is too permissive,
# allowing an attacker to instantiate an unexpected but "trusted" serializable class.

# --- Mock Serializable Classes ---
# In real LangChain, these would be various components like Prompts, Chains, Tools, etc.

class LangChainSerializable:
    """A mock base class to identify all 'trusted' serializable objects."""
    __lc_serializable__ = True
    _registry = {}

    def __init_subclass__(cls, **kwargs):
        """Registers all subclasses so the vulnerable loader can find them."""
        super().__init_subclass__(**kwargs)
        cls._registry[cls.__name__] = cls

class SimplePrompt(LangChainSerializable):
    """A benign, expected class for a given runtime path."""
    def __init__(self, text: str):
        self.text = text
        print(f"[*] Benign class 'SimplePrompt' instantiated with text: '{self.text}'")

class UnsafeFileTool(LangChainSerializable):
    """
    A 'trusted' but powerful class an attacker wants to instantiate.
    This represents a class with side effects, like a tool that can access the filesystem.
    """
    def __init__(self, file_path: str, content: str = ""):
        self.file_path = file_path
        self.content = content
        print(f"\n[!] DANGEROUS class 'UnsafeFileTool' instantiated.")
        print(f"[!] Attacker wants to write to file: '{self.file_path}'")
        self.execute()

    def execute(self):
        """The dangerous action."""
        try:
            with open(self.file_path, 'w') as f:
                f.write(self.content)
            print(f"[!!!] SUCCESS: Wrote to '{self.file_path}'")
        except Exception as e:
            print(f"[!!!] FAILED to write to file: {e}")


# --- Vulnerable Deserialization Logic (Pre-Fix) ---

def vulnerable_deserializer(serialized_payload: str):
    """
    Simulates the vulnerable LangChain deserialization path.
    It uses a broad allowlist (any subclass of LangChainSerializable)
    to revive objects, rather than a narrow list of expected types.
    """
    data = json.loads(serialized_payload)
    class_name = data.get("__type__")

    if not class_name:
        raise ValueError("Payload missing '__type__' field.")

    # The vulnerability: The loader looks up ANY registered serializable class.
    # It does not validate if `class_name` is appropriate for this specific context.
    cls = LangChainSerializable._registry.get(class_name)

    if cls:
        # Instantiates the class with attacker-controlled arguments.
        kwargs = {k: v for k, v in data.items() if k != '__type__'}
        return cls(**kwargs)
    else:
        raise TypeError(f"Cannot deserialize: Type '{class_name}' not found.")

# --- Application using the vulnerable deserializer ---

def process_run_output(payload_from_untrusted_source: str):
    """
    A simulated runtime path that expects a simple payload (like a SimplePrompt)
    but is vulnerable because it uses the unsafe deserializer.
    """
    print("--- Processing received payload ---")
    # The application code expects a benign object but gets a dangerous one.
    deserialized_object = vulnerable_deserializer(payload_from_untrusted_source)
    # The application might not even use the object, but instantiation is enough.
    print("--- Finished processing payload ---")


if __name__ == '__main__':
    # An attacker crafts a payload to instantiate the 'UnsafeFileTool' class
    # instead of the expected 'SimplePrompt' class.
    # The constructor arguments are also controlled by the attacker.
    
    # Use a non-destructive command for demonstration
    filename = "pwned_by_cve_poc.txt"
    content = "This file was created by exploiting CVE-2026-44843"
    
    malicious_payload = json.dumps({
        "__type__": "UnsafeFileTool",
        "file_path": filename,
        "content": content
    })

    print(">>> Sending malicious payload to the vulnerable application...")
    
    process_run_output(malicious_payload)

    if os.path.exists(filename):
        print(f"\n[SUCCESS] Vulnerability confirmed: The file '{filename}' was created.")
        os.remove(filename)
    else:
        print(f"\n[FAIL] Exploit did not work as expected.")

Patched code sample

import sys
from typing import List, Type, Any, Dict

# --- Mockup of LangChain Environment ---

# Represents a class that is expected and safe in a specific runtime context.
class ExpectedRunInput:
    def __init__(self, data: str):
        self.data = data
        print(f"Safely processed ExpectedRunInput with data: '{self.data}'")

# Represents another valid, serializable object that is NOT expected in this
# specific runtime path. An attacker might try to instantiate this to cause
# an unintended side effect.
class UnintendedSideEffectClass:
    def __init__(self, command: str):
        self.command = command
        # In a real exploit, this could access files, make network requests, etc.
        print(f"\n!!! VULNERABILITY EXPLOITED !!!")
        print(f"!!! Unexpected class instantiated with arguments: '{self.command}'")

# A simplified mock of LangChain's internal class mapping.
_CLASS_MAP = {
    ("expected_run_input",): ExpectedRunInput,
    ("unintended_side_effect",): UnintendedSideEffectClass,
}

# A simplified mock of the LangChain deserialization function `load()`.
# The vulnerability occurred in code paths that called this with a non-specific
# or overly broad `allowed_objects` list.
def load(data: Dict[str, Any], *, allowed_objects: List[Type] = None):
    class_path = tuple(data["id"])
    cls = _CLASS_MAP.get(class_path)

    if cls is None:
        raise ValueError(f"Class not found for id: {class_path}")

    # This is the core of the security check.
    if allowed_objects is not None and cls not in allowed_objects:
        raise ValueError(
            f"Loading of class '{cls.__name__}' is not allowed. "
            f"Allowed classes are: {[c.__name__ for c in allowed_objects]}"
        )

    return cls(**data["kwargs"])


# --- Demonstration of the Fix ---

def fixed_runtime_path_deserialization(payload: dict):
    """
    This function represents a fixed runtime path.
    It explicitly specifies ONLY the classes it expects to deserialize,
    preventing an attacker from instantiating other valid but unintended classes.
    """
    print(f"Attempting to deserialize payload with a restricted allowlist...")
    try:
        # THE FIX: A specific, minimal list is provided to `allowed_objects`.
        # The vulnerable code might have omitted this check or used a broad allowlist.
        allowed = [ExpectedRunInput]
        result = load(payload, allowed_objects=allowed)
        print("Deserialization successful and safe.")
        return result
    except ValueError as e:
        print(f"Deserialization blocked as expected: {e}", file=sys.stderr)
        return None

# Attacker-supplied payload attempting to instantiate an unexpected class.
attacker_payload = {
    "lc": 1,
    "type": "constructor",
    "id": ["unintended_side_effect"],
    "kwargs": {"command": "read_sensitive_data"}
}

# The fixed code path is called with the malicious payload.
# The deserialization will fail safely because UnintendedSideEffectClass
# is not in the explicit `allowed_objects` list.
fixed_runtime_path_deserialization(attacker_payload)

Payload

{
    "lc": 1,
    "type": "constructor",
    "id": ["langchain_community", "tools", "shell", "tool", "ShellTool"],
    "kwargs": {}
}

Cite this entry

@misc{vaitp:cve202644843,
  title        = {{LangChain insecure deserialization allows instantiation with untrusted arguments.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-44843},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-44843/}}
}
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 ::