Patch first. Then work out which applications could actually move attacker-shaped data through dumps() or dumpd() and later into load() or loads().
That order follows the primary sources. NVD describes CVE-2025-68664 as a serialization injection vulnerability in langchain-core. GitHub's reviewed advisory rates it critical at CVSS 9.3 and lists the patched versions as 0.3.81 and 1.2.5. LangChain merged the upstream serialization patch in pull request 34455.
The vulnerability details here come from NVD, GitHub and the upstream remediation. The detection guidance is a defensive adaptation based on Microsoft security operations and detection-engineering experience. It is not independent vulnerability research.
What failed at the trust boundary
LangChain uses an lc key to mark its serialized objects. Before the patch, dumps() and dumpd() did not escape a plain, user-controlled dictionary that contained the same key.
The vulnerable sequence was:
- Attacker-influenced data contained an
lcstructure. - The application passed that data through
dumps()ordumpd(). - The serialized data later reached
load()orloads(). - The loader interpreted the injected structure as a LangChain object instead of plain user data.
The GitHub advisory lists model response fields such as additional_kwargs and response_metadata as common sources of attacker-influenced content. Prompt injection is therefore one route into the vulnerable data flow, but the bug is still a serialization and deserialization flaw. A prompt alone is not evidence that a specific application reached the vulnerable loader.
What impact is supported by the advisory
GitHub documents two direct impact classes:
- extraction of environment-variable secrets when deserialization used
secrets_from_env=True, which was the old default; - instantiation of classes within trusted namespaces using attacker-controlled parameters, potentially triggering constructor side effects such as network or file operations.
The patch also introduced stricter defaults:
allowed_objects="core"limits the default object set;secrets_from_envchanged fromTruetoFalse;- an
init_validatorblocks Jinja2 templates by default.
Cyata's research discusses code-execution paths under additional conditions and says direct code execution from loads() alone was not confirmed in its Jinja2 path. The useful wording is therefore precise: the advisory supports secret extraction and controlled object instantiation. It does not support saying that every vulnerable installation gives immediate remote code execution.
Who needs to review exposure
GitHub lists these vulnerable flows, among others:
astream_events(version="v1");Runnable.astream_log();dumps()ordumpd()on untrusted data followed byload()orloads();- direct deserialization of untrusted data;
RunnableWithMessageHistory;InMemoryVectorStore.load()with untrusted documents;- untrusted cache generations;
- untrusted manifests from
hub.pull(); MultiVectorRetrieverwith byte stores containing untrusted documents.
Do not turn this into a checklist where package presence equals exploitability. Package presence establishes patch scope. Reachability and data flow establish application exposure.
Verify the installed package
Check the resolved environment, not only the declared requirement:
python -m pip show langchain-core
python -c "import langchain_core; print(langchain_core.__version__)"
For containers and serverless packages, run the check against the built artifact or software bill of materials. A lockfile in the repository does not prove the deployed image resolved the same version.
Upgrade on the relevant release line:
# 0.3 release line
python -m pip install "langchain-core>=0.3.81,<0.4"
# 1.x release line
python -m pip install "langchain-core>=1.2.5,<2"
Rebuild the deployable artifact and verify the version again after dependency resolution.
Defender XDR inventory starting point
If Defender Vulnerability Management surfaces the package in DeviceTvmSoftwareInventory, this query can group vulnerable versions. Coverage of Python packages depends on the environment and inventory integration, so absence is not proof of safety.
DeviceTvmSoftwareInventory
| where SoftwareName has "langchain"
| extend ParsedVersion = parse_version(SoftwareVersion)
| where
ParsedVersion < parse_version("0.3.81")
or (
ParsedVersion >= parse_version("1.0.0")
and ParsedVersion < parse_version("1.2.5")
)
| summarize
Devices = make_set(DeviceName, 100),
DeviceCount = dcount(DeviceName)
by SoftwareName, SoftwareVersion
| order by DeviceCount desc
Pair this with dependency scanning, container registry inventory and SBOM data. Python libraries often live in virtual environments or images that endpoint software inventory does not enumerate reliably.
Find likely runtime locations
Process telemetry can help identify where Python services mention LangChain. It cannot reveal the loaded package version or confirm exploitation.
DeviceProcessEvents
| where Timestamp > ago(30d)
| where FileName in~ (
"python.exe",
"python",
"python3",
"uvicorn",
"gunicorn"
)
| where ProcessCommandLine has_any (
"langchain",
"langgraph",
"langchain_core"
)
| summarize
FirstSeen = min(Timestamp),
LastSeen = max(Timestamp),
Accounts = make_set(AccountName, 20),
Commands = make_set(ProcessCommandLine, 20)
by DeviceId, DeviceName, FileName, FolderPath
| order by LastSeen desc
Use the result to find owners and deployment manifests. Do not convert it into an exploitation alert.
Review the actual code path
After patching, answer four concrete questions for each application:
- Can user input, retrieved content, tool output or model output populate a serialized dictionary?
- Does the application call
dumps()ordumpd()on that data? - Can the resulting bytes or text later reach
load()orloads()? - Which secrets, network routes, files and allowed classes are available to that process?
If the flow is unnecessary, remove it. If serialization is required, validate the data against a strict schema before it crosses the boundary. Keep unexpected keys out of model and tool outputs rather than trusting downstream code to interpret them safely.
Keep the patched defaults restrictive
Explicitly avoid environment-secret resolution for untrusted data:
from langchain_core.load import loads
value = loads(serialized_data, secrets_from_env=False)
Keep allowed_objects as narrow as the application permits. Do not disable the default init_validator for Jinja2 templates unless the serialized data is fully trusted and the application has a documented reason.
Migrate astream_events(version="v1") consumers to version 2. GitHub states that version 1 uses the vulnerable serialization path and version 2 does not.
What the detection can and cannot tell you
Inventory finds patch candidates. Process telemetry finds likely owners and runtimes. Neither proves that attacker-controlled data reached the vulnerable serialization flow.
Exploit assessment requires application logs, traces, serialized artifacts and code review. Look for unexpected lc structures in attacker-influenced fields, loader calls against those artifacts and outbound or file side effects from the affected process. Preserve the evidence before rotating secrets if the application may have exposed them.
The durable lesson is simple: data produced by a model or tool is still untrusted when it reaches a deserializer. Patch this CVE, then keep that boundary explicit for the next framework bug.
