And why the kill chain you have read about does not exist
On 27 March 2026, three vulnerabilities were disclosed across LangChain and LangGraph. Each exposes a different class of data, each has a different reachability condition, and the write-ups that followed mostly chained them into a single dramatic attack sequence that cannot happen.
This post does three things: explains each mechanism precisely, states the conditions under which it is actually reachable, and takes apart the chain narrative — because the real one is narrower, better documented, and more useful.
I build a scanner for this category, so at the end of each section I say which of my rules maps to the CVE and whether it would actually catch it in your code. In one case the answer is no.
CVE-2025-68664 — "LangGrinch," and the bug that was an absence
CVSS 9.3. langchain-core. Patched in 0.3.81 and 1.2.5.
LangChain serializes objects to JSON with dumps() and dumpd(), and restores them with load() and loads(). To know which JSON objects represent real LangChain classes, it marks them with a reserved key: lc.
The vulnerability is that dumps() did not escape that key when serializing free-form, user-controlled dictionaries.
So if user-influenced data contains a dictionary with an lc key, it gets written out looking exactly like a serialized LangChain object. Later, when something deserializes that payload, the loader treats it as a class to reconstruct rather than as data to return.
Yarden Porat at Cyata, who reported it, put the root cause better than I can paraphrase: the bug wasn't a piece of bad code, it was the absence of code. No escaping in the serialization path. Not a flaw in deserialization.
That distinction matters practically. If you audited your load() calls after reading about this — checking allowlists, checking secrets_from_env — you looked at the wrong end. The write happened earlier, probably in a logging or state-persistence path nobody thought of as security-relevant, and the payload sat there until something read it back.
Where it is reachable. Anywhere untrusted content can influence a dictionary that later gets serialized and then deserialized. In practice: hub.pull (pulled manifests are deserialized locally), byte-store-backed retrievers and document stores that persist serialized representations, and LangSmith run loaders processing untrusted messages. The prompt-injection route is real — get the model to emit JSON containing an lc key, let the application log it, wait for it to be read back.
The patch adds escaping in dumps/dumpd, flips secrets_from_env to False by default, and adds an allowed_objects allowlist to load. All three are the right changes; the escaping is the actual fix.
A companion issue in LangChain.js, CVE-2025-68665, CVSS 8.6, is the same class.
Lucin's rule: AG-DESERIALIZE, which detects deserialization of untrusted-influenced bytes. This class is at 100% recall (6/6) on my held-out corpus. It flags the load() side.
Honest limitation: flagging the load() side is flagging the symptom. Lucin will tell you that you deserialize data whose provenance it cannot establish, which is the right warning and is not the same as detecting the missing escape upstream. Detecting the actual bug would mean proving that a particular dictionary reaching dumps() is user-influenced, across whatever distance separates those two events in your application. That is a whole-program dataflow problem, and my analysis is intraprocedural. I would have warned you about the risky load(). I would not have found the bug.
dumps(), not loads(). That distinction is the whole advisory: hardening your deserializer does not help, because the payload was made trustworthy on the way out.CVE-2025-67644 — SQL injection where parameterising the value does not help
CVSS 7.3. langgraph-checkpoint-sqlite ≤ 3.0.0. Patched in 3.0.1.
LangGraph persists agent state as checkpoints. The SQLite checkpointer builds a WHERE clause for metadata filters in a function called _metadata_predicate(), and it built it like this:
predicate = f"metadata->>'$.{filter_key}' = ?"
# ^^^^^^^^^^ interpolated directly
The value is parameterised — that is what the ? is. The key is interpolated into the f-string.
This is the detail that makes the CVE worth reading rather than skimming. Almost every piece of SQL-injection advice you have absorbed is about parameterising values, and this code does that correctly. The injection point is the identifier, inside a json_extract path expression, and an attacker who controls the filter key breaks out of the JSON-path context and rewrites the statement.
Reached through SqliteSaver.list() or .alist(), which in a real application usually means get_state_history() exposed with a caller-controlled filter.
The impact is a complete filter bypass — every checkpoint record, including conversation state and thread IDs. For an agent, "all checkpoint records" is the memory of every conversation it has had.
Patched by enforcing a regex on keys: ^[a-zA-Z0-9_.-]+$. An allowlist, which is the correct fix for an identifier that cannot be parameterised.
Who is not affected, which is worth stating plainly: LangSmith Deployment (formerly LangGraph Platform) runs PostgreSQL and is not vulnerable to this. If you are on managed LangGraph, this one is not yours. Accuracy about who is safe is the cheapest credibility available in a security write-up, and most write-ups skip it.
Lucin's rule: AG-SQL, tainted parameter reaching a SQL execution sink. 100% recall on the SQL/CQL class (8/8 — 6 SQL, 2 CQL). It fires on this pattern because the interpolation and the sink are in the same function — which is exactly the shape my intraprocedural analysis is good at.
AG-SQL.CVE-2026-34070 — path traversal, and the constraint everyone omits
CVSS 7.5. langchain-core before 1.2.22. CWE-22.
Three functions in langchain_core.prompts.loading — load_prompt, load_prompt_from_config, and the .save() method on prompt classes — accept file paths from deserialized configuration dictionaries and read them without sanitizing path components. Supply ../../../ and you read outside the intended directory.
All three are undocumented legacy APIs, superseded by the dumpd/dumps/load/loads serialization APIs in langchain_core.load, which do not touch the filesystem. The patch deprecates them formally; removal is scheduled for 2.0.0.
Now the part that is usually left out.
The implementation validates file extensions. Reads are restricted to .txt, .json and .yaml.
That constraint substantially changes what this vulnerability is worth to an attacker, and it is the reason the next section exists.
Lucin's rule: AG-PATH-TRAVERSAL — and Lucin does not catch this today. The detector is built, sound and unit-tested, and it is deliberately unregistered, because the benign corpus contains byte-identical legitimate file tools and registering it would have produced noise. Path-traversal recall on my benchmark is 0%, on purpose. I wrote about that decision separately; the short version is that I would rather publish the gap than a number I cannot defend. If you need this class covered, Lucin is not the tool for it right now.
The kill chain you have read about does not exist
Several widely circulated write-ups present these three CVEs as a single escalation:
- Path traversal reads
.envfiles, stealing API keys and database credentials. - Those credentials open the checkpoint database, where SQL injection dumps agent memory.
- Serialization injection turns that into remote code execution.
It is a compelling narrative. Step one is impossible.
.env is not in the extension allowlist. The path-traversal function reads .txt, .json and .yaml. It cannot read .env, and it cannot read a Docker Compose file, which is the other example these write-ups reach for. A Kubernetes manifest with a .yaml extension is arguably reachable, and that is a genuine concern worth raising on its own terms — but it is not the example being made, and the example being made is the impossible one.
None of the three advisories describes this chain. It appears to be a reconstruction that acquired the authority of a documented attack by being repeated.
The real chain is better, and it is documented. Check Point Research published it: CVE-2025-67644 chained with CVE-2026-28277, an unsafe msgpack deserialization issue. SQL injection returns a malicious checkpoint row, that row gets deserialized, and you have remote code execution on a self-hosted LangGraph server. Two CVEs, one documented path, an actual proof of concept.
I am spending several hundred words on this because the correction is the most useful thing in the post. A fabricated kill chain does real damage: it sends people to audit the wrong function, it inflates a 7.5 into an infrastructure emergency, and when someone eventually checks the advisory and finds the extension allowlist, every other claim in the same article loses its warrant.
Read the advisory. It takes four minutes and it is the whole job.
Reachability, honestly
| Self-hosted, SQLite | Self-hosted, Postgres | Managed (LangSmith Deployment) | |
|---|---|---|---|
| CVE-2025-68664 (serialization) | Conditional — needs untrusted data reaching dumps() then load() |
Same | Same |
| CVE-2025-67644 (SQLi) | Reachable if filter keys are caller-controlled | Not applicable | Not affected |
| CVE-2026-34070 (traversal) | Conditional — needs the legacy loaders exposed to caller input | Same | Same |
| CVE-2026-28277 (msgpack) | Chains with 67644 → RCE | Not applicable | Not affected |
The pattern: the SQLite-specific issues are the sharpest and the most narrowly scoped. The serialization and traversal issues are broader in principle and depend entirely on whether your application exposes those particular functions to anything untrusted. "Do we call load_prompt on caller-supplied config?" is a five-minute grep and it is the highest-value thing you can do after reading this.
What to actually do
- Upgrade.
langchain-core≥ 1.2.22 covers both the traversal and the serialization issues;langgraph-checkpoint-sqlite≥ 3.0.1 covers the SQLi. Do this first, before any of the analysis below. - Grep for the legacy loaders.
load_prompt,load_prompt_from_config,.save()on prompt classes. If none of them touch caller input, 34070 was never yours. - Find your
load()calls and ask where the bytes came from. This is the 68664 question, and it is harder than it sounds because the write and the read are usually in different subsystems. Logging and state persistence are the paths people miss. - If you self-host LangGraph with SQLite, check whether any filter key reaches a checkpointer from a request. That is the 67644-plus-28277 chain, and it is the only one here with a documented route to RCE.
- Pin your dependencies. Not because it prevents these — it does not — but because the March 2026 LiteLLM compromise was delivered through unpinned resolution in exactly this ecosystem, and you are already in the file.
The thing these three have in common
None of them is an AI vulnerability.
One is a missing escape in a serializer. One is string interpolation into SQL. One is unsanitized path components. Textbook bugs, in a textbook order, of the kind application security has been cataloguing for twenty years.
What agent frameworks changed is not the bug classes. It is the reachability. An unsanitized path in a web application is reachable by whoever can craft a request. An unsanitized path in an agent tool is reachable by whoever can get text in front of the model — a document, a web page, a dataset card, an email, an issue comment. The attack surface is no longer your API; it is everything your agent reads.
That is why I build a scanner that reads tool bodies rather than tool names, and it is also why the three CVEs above are less interesting than the fact that a comment in a README can reach them.
Reproduce anything in this post
Scan your agent for these classes: pip install lucin && lucin scan .
The detectors that map to them: src/lucin/detectors/{insecure_deserialization,sql_injection,path_traversal}.py
Which classes we miss, and why: lucin.pages.dev/blog/what-we-miss/
Advisories. GHSA-qh6h-p6c9-ff54 (CVE-2026-34070) · GHSA-9rwj-6rc7-p77c (CVE-2025-67644) · Cyata on LangGrinch (CVE-2025-68664) · Check Point Research, SQLi to RCE · Tenable, CVE-2026-34070 · The Hacker News, on LangGrinch