← All posts
LangGrinch Alert: Critical LangChain Vulnerability CVE-2025-68664 - Detection and Response Guide

LangGrinch Alert: Critical LangChain Vulnerability CVE-2025-68664 - Detection and Response Guide

Sat Jan 31 updated Mon May 25 13 min read
vulnerabilitylangchainai-securitydefender-xdrkqlsupply-chaincve

A critical vulnerability just dropped in one of the most widely-deployed AI frameworks on the planet. CVE-2025-68664, dubbed "LangGrinch," affects LangChain Core's serialization mechanism and carries a CVSS score of 9.3. If you're running LangChain in production? With 98 million downloads last month alone, many of you are. You need to act now.

This isn't theoretical. The vulnerability allows secret extraction from environment variables, arbitrary object instantiation, and potentially remote code execution. Microsoft published detection guidance yesterday. I'm going to walk you through exactly what SOC teams need to know.

TL;DR: What You Need to Do Right Now

  1. Patch immediately: Update to langchain-core 0.3.81+ or 1.2.5+
  2. Hunt for vulnerable versions: Use the KQL queries below
  3. Assess exposure: Check if your AI workloads serialize/deserialize LLM outputs
  4. Monitor: Deploy detection rules for exploitation attempts

What is the LangGrinch vulnerability, technically?

LangChain Core's serialisation format reserves the lc key to mark objects that the framework itself produced — when the deserialiser sees {"lc": 1, "type": "constructor", ...} it treats the dict as a trusted LangChain object and reconstructs it. The pre-patch versions of dumps() and dumpd() did not escape user-controlled dictionaries that happened to also contain an lc key, so any path that round-trips an attacker-influenced dict through serialisation would, on deserialisation, materialise it as a trusted object — bypassing the framework's intended trust boundary.

The Attack Chain

  1. Injection: Attacker crafts malicious input containing an lc key
  2. Serialization: The input gets serialized through normal LangChain operations (logging, streaming, caching)
  3. Deserialization: When the data is later deserialized, LangChain treats the attacker's dict as a trusted object
  4. Exploitation: This leads to:
    • Environment variable extraction (including API keys, secrets)
    • Arbitrary object instantiation within approved namespaces
    • Potential code execution via Jinja2 template injection

Why prompt injection is the real attack vector

The most dangerous attack vector isn't someone sending you a malicious blob directly. It's prompt injection.

LLM responses can influence fields like additional_kwargs and response_metadata. These fields get serialized during streaming, logging, and message history operations. A single malicious prompt can cascade through your AI pipeline and trigger exploitation.

User prompt → LLM response (influenced) → Serialization → Deserialization → Secret theft

This is exactly the "AI meets classic security" intersection that catches organizations off guard. It also explains why traditional WAF-style input validation does not help — the malicious content arrives from your own model, on your own infrastructure, on a code path that has every right to call dumps().

How do I find vulnerable LangChain installs with Defender XDR?

The discovery path has two complementary queries: DeviceTvmSoftwareInventory for managed endpoints (covers both developer laptops and long-lived servers) and CloudProcessEvents for containerised workloads (catches ephemeral build steps and short-lived containers that finish before the next TVM sweep). Run both — endpoint-only or container-only coverage misses entire categories of deployment.

Defender XDR: Find Vulnerable Software

Microsoft published this KQL query to identify devices running vulnerable versions:

// Find devices with vulnerable LangChain versions
DeviceTvmSoftwareInventory
| where SoftwareName has "langchain"
    and (
        // 0.x versions below 0.3.81
        SoftwareVersion startswith "0."
        and (
            toint(split(SoftwareVersion, ".")[1]) < 3
            or (
                SoftwareVersion hasprefix "0.3."
                and toint(split(SoftwareVersion, ".")[2]) < 81
            )
        )
        // 1.x versions below 1.2.5
        or (
            SoftwareVersion hasprefix "1."
            and (
                toint(split(SoftwareVersion, ".")[1]) < 2
                or (
                    toint(split(SoftwareVersion, ".")[1]) == 2
                    and toint(split(SoftwareVersion, ".")[2]) < 5
                )
            )
        )
    )
| project DeviceName, OSPlatform, SoftwareName, SoftwareVersion
| summarize
    VulnerableDevices = dcount(DeviceName),
    Devices = make_set(DeviceName)
    by SoftwareName, SoftwareVersion
| order by VulnerableDevices desc

Defender for Containers: Cloud Process Monitoring

If you're running Defender for Containers, you can hunt for LangChain-related process activity in your containerized workloads using the CloudProcessEvents table:

// Find LangChain processes in containers (Defender for Containers)
CloudProcessEvents
| where Timestamp > ago(7d)
| where ProcessCommandLine has_any ("langchain", "langgraph", "lc_")
    or FileName has "python"
| where ProcessCommandLine has_any ("pip install", "import langchain")
| project
    Timestamp,
    AccountName,
    FileName,
    ProcessCommandLine,
    ContainerName,
    PodName
| order by Timestamp desc

Note: CloudProcessEvents is in Preview. For software inventory across endpoints, use the DeviceTvmSoftwareInventory query in the previous section.

Hunt for Suspicious Serialization Activity

This query looks for Python processes associated with LangChain making unexpected network connections. That's a potential indicator of secret exfiltration:

// Detect potential LangGrinch exploitation - outbound connections from Python/LangChain
DeviceNetworkEvents
| where Timestamp > ago(24h)
| where InitiatingProcessFileName in ("python", "python3", "python.exe")
| where InitiatingProcessCommandLine has_any ("langchain", "lc_", "langgraph")
| where RemoteIPType == "Public"
| where ActionType == "ConnectionSuccess"
| summarize
    ConnectionCount = count(),
    RemoteIPs = make_set(RemoteIP),
    RemoteUrls = make_set(RemoteUrl),
    FirstSeen = min(Timestamp),
    LastSeen = max(Timestamp)
    by DeviceName, InitiatingProcessCommandLine
| where ConnectionCount > 10 or array_length(RemoteIPs) > 3
| order by ConnectionCount desc

Hunt for Environment Variable Access

The primary exploitation path involves extracting environment variables. Look for suspicious env var access patterns:

// Monitor for suspicious environment variable access in Python processes
DeviceProcessEvents
| where Timestamp > ago(7d)
| where FileName in ("python", "python3", "python.exe")
| where ProcessCommandLine has_any ("os.environ", "getenv", "AWS_", "OPENAI_", "AZURE_", "API_KEY")
| where ProcessCommandLine has "langchain"
| project
    Timestamp,
    DeviceName,
    ProcessCommandLine,
    InitiatingProcessCommandLine,
    AccountName
| order by Timestamp desc

What are the vulnerable flows I should audit?

The advisory identifies 12 distinct vulnerable flows. Here are the most common in production environments, with the risk level and the technical reason each is exploitable. Treat this table as an audit checklist for your codebase — if any of these patterns appear, it is a deserialisation surface that needs review even after you patch:

Flow Risk Level Description
astream_events(version="v1") High v1 uses vulnerable serialization; v2 is safe
Runnable.astream_log() High Streaming logs trigger serialize/deserialize
dumps()loads() on untrusted data Critical Direct path to exploitation
RunnableWithMessageHistory High Message history serialization
InMemoryVectorStore.load() Medium Vector store deserialization
hub.pull() (LangChain Hub) Medium Manifest pulling from external source

Check Your Code

Search your codebase for these vulnerable patterns:

# Find potentially vulnerable code patterns
grep -rn "astream_events.*version.*v1" .
grep -rn "astream_log" .
grep -rn "dumps\|dumpd" . | grep -i langchain
grep -rn "RunnableWithMessageHistory" .
grep -rn "loads\|load" . | grep -i langchain

What does the remediation playbook look like?

Remediation is three phases on different clocks: a 24-hour patch window, a 72-hour code audit, and ongoing detection. The patch alone is necessary but not sufficient — most teams discover during the audit phase that they have at least one code path that should never have been deserialising LLM output in the first place, and that code path stays an attack surface for the next CVE in this class even after the current one is fixed.

Phase 1: Immediate Patching (0-24 hours)

Priority 1: Upgrade LangChain Core

# For 0.3.x users
pip install "langchain-core>=0.3.81"

# For 1.x users
pip install "langchain-core>=1.2.5"

# Verify the version
python -c "import langchain_core; print(langchain_core.__version__)"

Priority 2: Pin versions in requirements

# requirements.txt
langchain-core>=0.3.81,<0.4.0  # or >=1.2.5 for 1.x

Phase 2: Assess and Harden (24-72 hours)

  1. Audit secrets_from_env usage

    Before the patch, secrets_from_env=True was the default. Review your deserialization calls:

    # Unsafe (pre-patch default)
    loads(serialized_data, secrets_from_env=True)
    
    # Safe
    loads(serialized_data, secrets_from_env=False)
    
  2. Review LLM output handling

    Treat these fields as untrusted:

    • additional_kwargs
    • response_metadata
    • Tool outputs
    • Retrieved documents
  3. Check for astream_events v1 usage

    Migrate to v2:

    # Vulnerable
    async for event in runnable.astream_events(input, version="v1"):
        ...
    
    # Safe
    async for event in runnable.astream_events(input, version="v2"):
        ...
    

Phase 3: Detection Deployment (72+ hours)

Deploy the KQL queries above as scheduled analytics rules in Microsoft Sentinel or as custom detection rules in Defender XDR.

Example Sentinel rule configuration:

name: LangGrinch - Vulnerable LangChain Detected
severity: High
tactics:
  - InitialAccess
  - Execution
query: |
  DeviceTvmSoftwareInventory
  | where SoftwareName has "langchain"
  | where SoftwareVersion !startswith "0.3.81"
      and SoftwareVersion !startswith "1.2.5"
  // ... (full version check logic)
triggerThreshold: 0
frequency: 1d

The bigger picture: AI supply chain security

This vulnerability highlights a critical blind spot in most organizations: AI framework security.

Yesterday, I wrote about securing AI data with Microsoft Purview. LangGrinch shows the other side of the coin. It's not just about protecting data in AI, but protecting your infrastructure from AI frameworks themselves. The AI agent identity crisis is the same problem at the credential layer — too much trust placed in software whose behaviour is shaped by attacker-controlled input.

Key questions for your AI security posture:

  1. Inventory: Where are you running AI/LLM frameworks?
  2. Versioning: Can you quickly identify which versions are deployed?
  3. Trust boundaries: Do you treat LLM outputs as untrusted input?
  4. Serialization: What data crosses serialize/deserialize boundaries?

If you can't answer these questions quickly and confidently, you're flying blind when advisories like this drop.

Technical deep dive: the lc marker confusion

For those who want to understand the mechanics:

LangChain's serialization format uses special keys to encode object metadata:

# Normal serialized LangChain object
{
    "lc": 1,
    "type": "constructor",
    "id": ["langchain_core", "messages", "HumanMessage"],
    "kwargs": {"content": "Hello"}
}

The vulnerability: if user-controlled data contains an lc key, the deserializer treats it as a trusted object:

# Attacker-controlled data
malicious_dict = {
    "lc": 1,
    "type": "secret",
    "id": ["OPENAI_API_KEY"]  # Extracts env var!
}

# This gets serialized and later deserialized...
# The framework thinks it's a real LangChain object

The patch wraps user dictionaries containing lc to prevent confusion:

# Post-patch: user dict with "lc" is escaped
{
    "lc": 1,
    "type": "not_implemented",
    "repr": "<wrapped user dict>"
}

This is the right structural fix for this specific bug class — the framework now refuses to materialise an object from a dict whose lc marker it did not write itself. It does not, however, change the broader risk that any deserialiser fed with adversarial input is a code-execution surface waiting for the next escape.

JavaScript/TypeScript Alert

There's a parallel vulnerability in LangChainJS: CVE-2025-68665 (GHSA-r399-636x-v7f6). Same mechanics, same risks. If you run both Python and JavaScript LangChain stacks, patch both, and rerun the audit checklist against your TypeScript codebase — the equivalent of astream_events(version="v1") and RunnableWithMessageHistory exists on the JS side too.

Key Takeaways

  • CVE-2025-68664 is critical (CVSS 9.3). Patch to 0.3.81+ or 1.2.5+ immediately.
  • The attack vector is subtle. Prompt injection can trigger exploitation through normal framework operations.
  • Detection is possible. Use the KQL queries to find vulnerable versions and suspicious activity.
  • LLM outputs are untrusted input. Treat additional_kwargs, response_metadata, and tool outputs accordingly.
  • AI supply chain security matters. Know where your AI frameworks are deployed and what versions you're running.

The frameworks we use to build intelligent applications are becoming attack surfaces themselves. LangGrinch won't be the last vulnerability of this kind.


References:


Questions? Connect on LinkedIn or see my CV.

Key takeaways

  • Patch `langchain-core` to 0.3.81+ (0.x line) or 1.2.5+ (1.x line) — both fix the `lc`-key confusion.
  • Prompt injection is the real attack vector: a malicious LLM response can populate `additional_kwargs`/`response_metadata` with an `lc`-keyed dict, and normal streaming serialises it.
  • The companion advisory CVE-2025-68665 affects LangChainJS — patch both if you run Python and JavaScript stacks.
  • Use `DeviceTvmSoftwareInventory` for endpoint coverage and `CloudProcessEvents` (preview) for containerised workloads to find vulnerable versions.
  • Set `secrets_from_env=False` on `loads()` and migrate `astream_events` callers from version v1 to v2 — both are documented vulnerable flows.

FAQ

What is CVE-2025-68664 (LangGrinch)?

CVE-2025-68664 — nicknamed 'LangGrinch' — is a CVSS 9.3 deserialization vulnerability in LangChain Core. LangChain encodes its own objects with a reserved `lc` key when serialising. Before the patch, `dumps()` and `dumpd()` did not escape user-controlled dictionaries that happened to contain an `lc` key, so an attacker could smuggle a forged 'LangChain object' through serialisation and have it deserialised as trusted. Exploitation paths include environment-variable extraction (API keys, cloud credentials), arbitrary object instantiation within allowed namespaces, and template-injection paths to code execution.

Which LangChain versions are vulnerable and what should I upgrade to?

All `langchain-core` versions below 0.3.81 in the 0.x line are vulnerable, and all `langchain-core` versions below 1.2.5 in the 1.x line are vulnerable. Upgrade to 0.3.81 or higher (for 0.x) or 1.2.5 or higher (for 1.x). Pin the constraint in `requirements.txt` so a future install does not silently regress, and verify with `python -c "import langchain_core; print(langchain_core.__version__)"`. LangChainJS has a parallel advisory (CVE-2025-68665) — patch both ecosystems if you use both.

How does prompt injection trigger LangGrinch?

An attacker does not need to hand-craft a serialised payload — they can inject the malicious dict through normal LLM output. LangChain models expose fields like `additional_kwargs` and `response_metadata` that get serialised during streaming, logging, and message-history operations. A prompt that coaxes the model to emit a dict containing an `lc` key (often via tool-output reflection or retrieved-document content) is enough to plant the payload. When the conversation is later replayed through `loads()`, the forged object is materialised.

How do I find vulnerable LangChain installs with Defender XDR?

Use `DeviceTvmSoftwareInventory` for managed endpoints. The query in this post checks for `langchain-core` versions below 0.3.81 (0.x line) or below 1.2.5 (1.x line). For containerised workloads, Defender for Containers exposes `CloudProcessEvents` (preview) which captures process command lines including `pip install` and `import` activity. Combine the inventory query with a process-level hunt to cover both long-lived installs and ephemeral build-time installs that vanish before TVM next scans.

What is the safest way to handle LLM outputs after this patch?

Treat every field that originates from the model as untrusted input. Specifically: never pass model output directly into `loads()` with `secrets_from_env=True` (which was the pre-patch default); avoid `astream_events(version="v1")` and migrate to `v2`; audit any code path that round-trips a `RunnableWithMessageHistory` through serialisation; and prefer strict schemas (Pydantic models with `extra='forbid'`) for tool-output handling so unexpected keys raise rather than silently propagate.

Does the patch fully eliminate the attack class?

It eliminates the specific `lc`-key confusion that CVE-2025-68664 describes, by wrapping user dictionaries containing `lc` so they cannot be re-interpreted as LangChain objects. The broader attack class — deserialising data derived from untrusted LLM output — is structural and survives the patch. The right mitigation is to limit deserialisation to data your application produced itself, and to put schema validation between any LLM/tool boundary and any deserialiser.