
Author: Yarden Porat
Cyata Research: LangGrinch Vulnerability in LangChain
Published: https://cyata.ai/blog/langgrinch-langchain-core-cve-2025-68664/

Yesterday LangChain published a critical security advisory for a vulnerability I discovered in langchain-core: CVE-2025-68664 / GHSA-c67j-w6g6-q2cm.
Earlier this year, my research focused on breaking secret managers in our "Vault Fault" work – systems specifically designed as a security boundary around your most sensitive credentials. One finding kept repeating itself: when a platform accidentally processes attacker-controlled data as a trusted structure, that boundary collapses fast. This time the system that "breaks" is not your secret manager. It’s the agent framework that may use them.
Why this vulnerability deserves special attention:
It’s in the core. This is not a tool-specific bug, not an integration edge case, and not "some community package did something weird." The vulnerable APIs (dumps() / dumpd()) reside in langchain-core itself.
The blast radius is enormous. By download volume, langchain is one of the most widely deployed AI framework components worldwide today. As of late December 2025, public package telemetry shows hundreds of millions of installs, with pepy.tech reporting ~847M total downloads and pypistats showing ~98M downloads in the last month.
One prompt can trigger many mechanisms. The most common real-world path here is not "an attacker sends you a serialized blob and you call load()." It’s subtler: LLM outputs can influence fields like additional_kwargs or response_metadata, and these fields can be serialized and later deserialized through normal framework facilities such as streaming logs/events. In simple terms, this means an exploit can be launched by a single text prompt that cascades into an unexpectedly complex internal pipeline.
Before you continue reading, patches have already been released in versions 1.2.5 and 0.3.81. If you are using LangChain in production, this is more complex than it may seem; please update as soon as possible.
LangChain uses a special internal serialization format where dictionaries containing the marker 'lc' represent LangChain objects. The vulnerability was that dumps() and dumpd() did not properly escape user-controlled dictionaries that accidentally included the reserved key 'lc'.
Thus, once an attacker can make a LangChain orchestration loop serialize and later deserialize content including the key 'lc', they can instantiate an unsafe arbitrary object, potentially triggering many attacker-friendly paths.
The advisory lists 12 distinct vulnerable flows that are extremely common in real-world use cases such as standard streaming events, logging, message history/memory, or caching:

The most severe impacts include:
Exfiltration of secrets from environment variables. The advisory notes this happens during deserialization with secrets_from_env=True. Notably, this was the default until yesterday. 🙂
Object instantiation in pre-approved namespaces (including langchain_core, langchain_openai, langchain_aws, langchain_anthropic…), potentially triggering side effects in constructors (network calls, file operations, etc.).
Under certain conditions, instantiation of LangChain objects can lead to arbitrary code execution.
This is classified under CWE-502: Deserialization of Untrusted Data, with a CNA CVSS score of 9.3 (Critical).
On Christmas Eve I was doing the least festive work: looking at serialization code and asking "wait… why is this considered trusted?"
Security research often looks dramatic from the outside. In reality it’s usually careful reading, small hypotheses, and slow accumulation of "that’s odd" moments.
It started the way many things do at Cyata: with a simple question we constantly ask when evaluating AI stacks for real risk:
Where are the trust boundaries in AI applications, and do builders actually know where those boundaries are?
LangChain is a powerful framework, and like most modern frameworks, it has to move complex structured data around: messages, tool calls, streaming events, traces, caches, and "runnables."
Looking through prior research, there was already extensive research on LangChain tools and integrations, but very few findings in the core library.
I started the research by working backwards. Finding interesting places (sinks), then figuring out how an attacker could reach them. Deserialization was an obvious target.
It took me quite a while to find something significant. But after some time I found that, assuming an attacker-controlled deserialization primitive, I could trigger a blind SSRF which could be used to exfiltrate environment variables (to be detailed soon). Since the result was limited to secret exfiltration rather than my primary goal of RCE, I continued auditing deserialization and took my time.
The bug was not a piece of bad code, it was missing code. dumps() simply didn't escape user-controlled dictionaries containing the key 'lc'. Escaping missing in the serialization path, not deserialization.

It's much easier to spot something wrong than to spot something missing, especially when you're auditing load(), not dumps(). In one of the most heavily audited AI frameworks. For two and a half years.
From there the research became a structured exercise:
Identify where untrusted content (mainly arbitrary dictionaries) enters serialization (LLM outputs, prompt injection, user input, external tools, retrieved documents).
Identify when those serialized data are deserialized.
Identify what an attacker can achieve from arbitrary object instantiation.
At that point the core finding was clear enough and actionable for responsible disclosure: there was an escaping gap in dumps() / dumpd() around dictionaries with the 'lc' key.
The advisory later captured what we often see in practice: fields like additional_kwargs and response_metadata can be influenced by LLM output and prompt injection, and these fields can be serialized-deserialized in many flows.
Credit to the LangChain team: the response and follow-up were decisive, not just patching the bug but also tightening defaults that were too permissive for the world we now live in.
The LangChain project decided to award a $4,000 USD bounty for this finding. According to huntr, the platform where LangChain ran its bounty program, this would be the highest amount ever awarded in the project, with bounties until now up to $125.
LangChain serializes certain objects using a structured dictionary format. The key 'lc' is used internally to indicate "this is a serialized LangChain structure," not just arbitrary user data.
This is a common pattern, but it creates a security invariant: Any user data that can contain 'lc' must be handled carefully. Otherwise an attacker can craft a dictionary that "looks like" an internal object and trick the deserializer into granting it meaning.
The patch makes the intention explicit in the updated documentation: during serialization, plain dictionaries containing the key 'lc' are escaped by wrapping them.
This prevents these dictionaries from being confused with actual serialized LangChain objects during deserialization.
LangChain's load()/loads() functions do not instantiate arbitrary classes – they check against an allowlist that controls which classes can be deserialized. By default, this allowlist includes classes from langchain_core, langchain_openai, langchain_aws, and other ecosystem packages.
Here's the catch: most classes in the allowlist have innocuous constructors. Finding exploitable paths required digging through the ecosystem for classes that do something significant upon instantiation. Those I found are detailed below, but there may be others waiting to be discovered.
LangChain's loads() function supports a secret type that resolves values from environment variables during deserialization. Before the patch, this secrets_from_env feature was enabled by default:
if (
value.get("lc") == 1
and value.get("type") == "secret"
and value.get("id") is not None
):
[key] = value["id"]
if key in self.secrets_map:
return self.secrets_map[key]
if self.secrets_from_env and key in os.environ and os.environ[key]:
return os.environ[key] # <-- Returning environment variable
return None
If the deserialized object is returned to the attacker, such as message history within an LLM context, this could leak environment variables.
But a more interesting path is indirect prompt injection. Even an attacker who cannot see any LLM responses can exfiltrate secrets by instantiating the right class. ChatBedrockConverse from langchain_aws is both in the default allowlist of loads and makes a GET request when constructed. The GET endpoint is attacker-controlled, and a specific HTTP header can be filled with an environment variable through the secrets_from_env feature.

This validator runs when ChatBedrockConverse is instantiated. The attacker controls endpoint_url, triggering an outbound request. In combination with secrets_from_env, the header aws_access_key_id can be filled with any environment variable – not only AWS keys.
We deliberately do not publish a full exploit here to give security teams time. In a few months the Huntr site will publish them automatically.
Among the classes in the default allowlist of loads() is PromptTemplate. This class creates a prompt from a template, and one of the available template formats is Jinja2.
When a template is rendered with Jinja2, arbitrary Python code can be executed. We did not find a way to trigger this from loads() alone, but if a subsequent call to the deserialized object triggers rendering, code execution follows.
We suspect there may be paths to direct code execution from loads(), but we have not confirmed any yet. If you have a solid idea or a test-worthy lead, we'd love to hear – this is exactly where the security community helps turn hypotheses into proof. 🤝
It's also worth noting: in past versions, the Chain class was also in the allowlist. This class had special capabilities that could allow a flow to template rendering.
Your application is potentially vulnerable if it uses vulnerable versions of langchain-core. Here are some of the most common vulnerable patterns (12 flows total identified):
Nevertheless, the system behavior is complex enough that it's risky to assume a quick code review will catch every reachable variant. The safest bet is to update to the patched version and not assume you are safe until you do.
Also, the advisory notes what I consider the most important real-world point:
The most common attack vector occurs through LLM response fields like additional_kwargs or response_metadata, which can be controlled via prompt injection and then serialized/deserialized in streaming operations.
This is exactly the kind of cross-section of "AI meets classic security" that catches organizations off guard. LLM output is untrusted input. If your framework processes chunks of that output as structured objects later, you must assume attackers will try to craft them.
Update langchain-core to the patched version. If you use langchain, langchain-community, or other ecosystem packages, check which version of langchain-core is actually installed in production environments.
Treat additional_kwargs, response_metadata, tool outputs, retrieved documents, and message history as untrusted unless proven otherwise. This is especially important if you stream logs/events and later rehydrate them with a loader.
Even after updating, stick with the principle: do not enable secret resolution from environment variables unless you trust the serialized input. The project changed defaults for a reason.
Based on my report, there is a closely related advisory in LangChainJS (GHSA-r399-636x-v7f6 / CVE-2025-68665) with similar mechanisms: confusion of the 'lc' marker during serialization, allowing secret exfiltration and unsafe instantiation in certain configurations.
If your organization runs both Python and JavaScript LangChain stacks, treat this as a reminder that the pattern crosses ecosystems: marker-based serialization, untrusted model output, and subsequent deserialization is a recurring form of risk.
We are entering a phase where AI agent frameworks become critical infrastructure inside production systems. Serialization formats, orchestration pipelines, tool execution, caches, and tracing are no longer "plumbing" – they are part of your security boundary.
This vulnerability is not "just a library bug." It is a case study of a larger pattern:
Your application may deserialize data it believes is safely produced.
But that serialized output can contain fields influenced by untrusted sources (including LLM outputs shaped by prompt injection).
A single reserved key used as an internal marker can become a pivot point into secrets and execution-adjacent behaviors.
At Cyata, our work is to help organizations build visibility, risk assessment, control, and governance around AI systems – because if you cannot quickly answer where agents are running, what versions are deployed, and what data flows through them, you are effectively flying blind when advisories like this land.
If you are a security leader reading this, here's the uncomfortable truth:
Most organizations currently cannot answer, quickly and confidently:
Where are we using agents?
What versions are deployed in production?
Which services have access to sensitive secrets?
Where do LLM outputs cross those boundaries?
This is not a "developer problem." It is a visibility and governance problem.
And that's where Cyata comes in.
At Cyata, we focus on practical outcomes: reducing AI and agent risk without slowing down builders. Vulnerabilities like this are rarely "just patch." They reveal gaps in how teams discover where agents run, understand real trust boundaries, and enforce safer defaults in fast-moving frameworks.
Know what is running, where, and how it is connected.
Quickly answer the first CVE question: are we affected, and in which flows?
Discover agent runtimes and integrations across environments (IDEs, CI, services, worker jobs, hosted agents).
Track frameworks, packages, and versions in use.
Prioritize what matters based on real blast radius, not just "library present."
Enable faster triage: what is internet-facing, what touches secrets, what runs with elevated privileges.
Identify highest-risk paths: untrusted content flowing into privileged contexts (services with secrets, broad tool permissions, production network access).
Highlight where "structured fields" can cross trust boundaries (metadata, tool outputs, streaming events, cached artifacts).
Reduce exposure even before every dependency is patched everywhere.
Encourage safer operational defaults: least privilege, isolation boundaries, and policy checks that scale across teams.
Enforce gateways around risky patterns (e.g., deserialization of untrusted data, permissive object rehydration, unsafe streaming-to-cache-to-rehydration flows).
Gate or restrict sensitive capabilities in untrusted contexts (e.g., secret access from environment, high-privilege tool execution, or running risky code paths in privileged workers).
Make "safe agent usage" repeatable, auditable, and hard to drift.
Define policies for approved frameworks, versions, and configurations.
Track and time-bound exceptions with owners and justification.
Monitor drift and risky feature usage over time, with an audit trail to support security reviews and compliance.
When a Christmas advisory drops, the goal is not heroics – it’s a calm, controlled response backed by real inventory and enforced gateways.
Report submitted via Huntr – December 4, 2025
Acknowledged by LangChain maintainers – December 5, 2025
Advisory and CVE published – December 24, 2025