The archive/CVE-2025-68664
langchain-coreDisclosed 2025

CVE-2025-68664:langchain-core: Unsafe deserialization

A historical CVE, retraced through the questions Lachesis asks of vulnerable code.

Historical recordlangchain-core==0.3.80 (affected: <0.3.81 and <1.2.5)
WeaknessCWE-502 → CWE-94 → CWE-95 · Unsafe deserialization
ImpactA unsafe deserialization in langchain-core, reachable from attacker-controlled input.
Read the reconstruction
01What happened

Attacker-controlled serialized data was revived as a class, turning a data-loading path into object construction.

That is the shape of the failure. The rest of this case file follows the evidence behind it.

Separate the record from the run

What was known.
What Lachesis established.

01Historical record

Known before Lachesis ran

The CVE and vulnerable release were selected from public history. We are not claiming novel discovery.

CVE
CVE-2025-68664
Vulnerable target
langchain-core==0.3.80 (affected: <0.3.81 and <1.2.5) · vulnerable
Prior knowledge
CVE-2025-68664 · langchain-core==0.3.80 (affected: <0.3.81 and <1.2.5) is vulnerable
then
02Lachesis run
Verified captured run

Independent re-detection

The experiment graph was built around the vulnerable module (release langchain-core==0.3.80, libs/core/langchain_core/load/load.py), rather than starting from arbitrary langchain-core source. We did not point the tool at the CVE. Enumeration ran over the whole candidate registry (candidate_census across every family); injection.exec was not singled out, and Lachesis surfaced the Reviver load sink on its own. The sink, its CWE, and its rank were emitted by the tool; the sink was not fed from memory.

Seeded inputs
None
Run timestamp
2026-08-24T12:06:09Z
Evidence artifact
~/.lachesis/graphs/langchain_load.kuzu

The commands and outputs below come from this recorded Lachesis run.

The reconstruction, in one frame

What Lachesis reconstructed.

One historical repository. Four captured queries. A complete source-to-sink argument.

Lachesis Casefiles · Unbound ComputeVerified runCVE-2025-68664
Serialized data came back as a class and reached module loading.
01Sourcereturn json.loads(text, object_hook=Reviver(...)) # loads(text) / load(obj) revives every dict leaf
02Sinkmod = importlib.import_module(".".join(namespace)) # then cls = getattr(mod, name); return cls(**kwargs)
03Missing guardif namespace[0] not in self.valid_namespaces or namespace == ["langchain"]: raise ValueError
$ lachesis sources_of → reaches → guard_dominance
langchain-core · langchain-core==0.3.80 (affected: <0.3.81 and <1.2.5) · vulnerableunboundcompute.com
This was known history. Your code is not.

Lachesis follows the same evidence path through unfamiliar repositories.

The Lachesis reconstruction

Here is how the path becomes visible.

These are the recorded questions Lachesis asked of the historical vulnerable code, followed by the raw result and source location each query returned.

Repositorylangchain-core
Revisionlangchain-core==0.3.80 (affected: <0.3.81 and <1.2.5) · vulnerable
EngineLachesis

Captured runThis block reflects the real blind drive of the graph we built for langchain-core. We loaded the whole graph, listed every bug family with nothing chosen ahead of time, and the code injection sink came out of that. The handles, model tag and guard reading below are the facts captured on that drive on that drive. No attacker JSON was seeded; the finding was rediscovered from graph structure, so the mode is independent-redetection.

01 · load_graph01 / 05
Lachesis ran
load_graph --path ~/.lachesis/graphs/langchain_load.kuzu --profile all && candidate_census

Load the graph and list every bug family

First we load the graph and ask the tool to list every bug family it knows, with nothing picked ahead of time. It reports all 8 domains and 31 sink constructors and says the census is complete for what the graph can observe. Injection is one row among all of them.

Captured resultCaptured · load_graph
raw captured outputload_graph
[lachesis-mcp] loaded the graph; overlay: 853 derived edges; dataflow tier: present
{"move": "load_graph", "graph": "/Users/riyandhiman/.lachesis/graphs/langchain_load.kuzu", "profile": "all"}

CANDIDATE_CENSUS
move: candidate_census
taxonomy (8):
  domain=lifecycle         title=Resource lifecycle              enumerable=True
  domain=memory            title=Memory safety                   enumerable=True
  domain=injection         title=Injection                       enumerable=True
  domain=navigation        title=Request forgery & redirection   enumerable=True
  domain=object-integrity  title=Object integrity                enumerable=True
  domain=filesystem        title=Filesystem                      enumerable=True
  domain=crypto-config     title=Cryptography & transport config enumerable=True
  domain=resource          title=Resource exhaustion             enumerable=True
constructors (31): complete_for_observable_graph=True
applied: True
libs/core/langchain_core/load/load.pyline 0
Code location returned for this evidence step.
The repair

A small line with a large consequence.

The allowlist checks only the top-level package name, not the submodule, class, or kwargs. It does not stop object construction within an allowed package.

Before
if namespace[0] not in self.valid_namespaces or namespace == ["langchain"]: raise ValueError
After the patch
// The dumps() side did not escape user-controlled mapping keys beginning with 'lc', letting an attacker smuggle a crafted {"lc":1,"type":"constructor","id":[...],"kwargs":{...}} node that load()/loads() then revives -- importing a module by the attacker-influenced namespace path and instantiating the named class with attacker kwargs. Fixed 0.3.81/1.2.5 by escaping 'lc' keys on serialization and hardening the load defaults (secrets_from_env=False plus an explicit allowed-objects allowlist).
Sources & further reading

The original record.

Read the historical advisory and vulnerability record behind this reconstruction.

Open the Lachesis quickstart