BlueHat IL 2026 · Check Point · formerly Cyata

Déjà Vuln

When Classic Exploits Hit AI Agents

Yarden Porat Shahar Tal

We went looking for the future.

We found the past waiting for us.

Déjà Vuln.

whoami

Cyata is now Check Point

Cyata — the control plane for agentic identity — was acquired by Check Point. The team now leads agentic security research there.

Shahar Tal

Head of Agentic Security Innovation, Check Point · ex-CEO & co-founder, Cyata

Yarden Porat

Security Research, Check Point · ex-Cyata

The premise

The Agentic Attack Surface

Every new agent capability is a new input boundary.

Tools

Args are attacker-influenced.

Memory / State

Serialized & restored.

RAG / Documents

Untrusted files & URLs.

Checkpoints

Save / resume = deserialize.

Why the core, not the tools

We didn't hunt the tools. We hunted the core.

Tool abuse

Scoped, sandboxed, under active scrutiny. Needs a specific tool + valid config. The tool surface is shrinking.

Framework core

Always on, rarely audited. Every app on the framework inherits it. Serialization, state, loaders — expanding.

The bet We wanted one finding that makes every agent vulnerable — so we went below the tools, into the agent harness (the orchestration loop).
The research

What we looked at

290M LangChain monthly downloads
52K CrewAI GitHub stars
12 CVEs across 6 attack paths
2 Attack vectors today
LangChain Microsoft Agent Framework CrewAI
The agentic stack rebuilt the early web’s attack surfacedeserialization, SSRF, file read, native parser bugsand handed the trigger to a language model

A small game

Guess the year

bug-classes.log
# four findings, same week Insecure deserialization SSRF to a metadata endpoint Arbitrary file read Memory corruption in a C parser

2008? Correct.

Last Tuesday, in a production AI agent? Also correct.

ATTACK VECTOR #1

Serialization Injection

LangChain  ·  Microsoft Agent Framework

LangChain · how it serializes

A tiny, trusting format

serializable.py
{"lc": 1, "type": "constructor", "id": ["langchain", "chat_models", "ChatOpenAI"], "kwargs": {"model": "gpt-4o", "temperature": 0}}

"lc" — version marker

Schema version. Trusted blindly — the gate that lets the rest of the blob in.

"type" — node kind

constructor · secret · not_implemented

"id" — class path

Drives the import — which module & class to instantiate.

"kwargs" — constructor args

Passed straight into the resolved class on rebuild.

LangChain · the flaw

Nobody escaped two letters

dumps() / dumpd() serialized attacker-controlled plain dicts verbatim — including the reserved "lc" key.

On reload, load() couldn't tell a forged marker from a genuine serialized object.

Note Root cause is the DUMP side — the deserializer worked as designed. The fix added an escape wrapper __lc_escaped__.
forged_payload.json
# attacker-supplied plain dict { "lc": 1, "type": "constructor", "id": ["...attacker.class.path..."], "kwargs": { ... } } # survives dump → reloads as object
LangChain · impact

Leaking environment variables

forged_payload.json
# a forged "secret" marker in untrusted input { "lc": 1, "type": "secret", "id": ["AWS_SECRET_ACCESS_KEY"] } ▼ load(secrets_from_env=True) load(...) -> os.environ[key] # langchain_core/load/load.py:481
Arbitrary secret read An attacker-named env var is resolved through os.environ and returned in the deserialized object — turning untrusted JSON into a direct read of the process secret store.
déjà: insecure deserialization trigger: agent state load

LangChain · the irony

The feature that redacts secrets exfiltrates them

By design — protect

On dump, _replace_secrets deletes the real key and writes a reference {"type":"secret","id":["OPENAI_API_KEY"]}. The secret never enters the blob — it's re-fetched from env at load.

The exploit — steal

load() resolves any incoming secret marker via os.environ[id] — and id is attacker-controlled. Name a variable instead of redacting one.

Same code, opposite beneficiary Redaction-and-rehydrate becomes name-and-exfiltrate.

LangChain · the twist

The attacker never calls load()

Prompt injection

You supply the payload as plain text.

Lands in metadata

additional_kwargs / response_metadata

Stream replays it

astream_events(v1) / astream_log()

Framework reloads

It round-trips & deserializes it for you.

Concierge-tier exploitation You supply the payload; the framework handles serialization, storage, and reload — no load() call required.

Disclosure

CVE-2025-68664 — “LangGrinch”

CWE-502 Deserialization CVSS 9.3 GitHub / 8.2 NVD Affected: langchain-core <1.2.5 & <0.3.81 Fixed: 1.2.5 / 0.3.81 (Dec 2025) Credit: Cyata (now Check Point)
Impact Allowlist-bounded object instantiation + secret resolution — potential RCE / SSRF, not confirmed pickle-grade RCE.

Refs: GHSA-c67j-w6g6-q2cm  ·  NVD

Methodology

Was it hard to find?

Given the file

We pointed an Opus 4.8 agent at load/ with a clear prompt. It found both bugs in one shot — code-only, working PoCs, high confidence.

Given the framework

Generic “find the top vulns” across all of langchain-core — it still ranked the serialization bugs #1 and #2.

The uncomfortable part It isn't hard to find — it was never pointed at. Frontier models surface these cold. Déjà vuln, meet AI-assisted review.
Microsoft Agent Framework · Checkpoints

Save state. Resume later.

Checkpoints are Git for an agent run — save, branch, rewind, resume. The edit-and-resend you already use in ChatGPT / Claude / Cursor.

Long-running workflows

Survive failures — restart from the last checkpoint instead of the beginning.

Audit & compliance

Capture and retain execution state for review and regulatory requirements.

Pause & resume

Suspend a workflow mid-run and continue later on demand.

Migrate environments

Move saved state across instances — rehydrate on a different machine.

Microsoft Agent Framework · the pickle inside

JSON on the outside, pickle on the inside

_workflows/_checkpoint_encoding.py
# checkpoint value, serialized as plain JSON { "__pickled__": "gASV...base64...", "__type__": "module:QualifiedName" <-- attacker-influenced } # line 304 — decode then revive the object pickled = _base64_to_unpickle(value) obj = pickle.loads(pickled)

A checkpoint travels as innocent JSON. But the __pickled__ field carries a base64 blob, and the __type__ marker names the class to revive — both fed straight into pickle.loads.

Python only The .NET path uses System.Text.Json — no pickle, no deserialization gadget.

Serialize → store → deserialize

Same shape, both frameworks

LangChain and Microsoft Agent Framework serialization flow
Microsoft Agent Framework · the control

A bouncer who checks ID after you leave

A type allowlist was added — but _verify_type runs after the object is already unpickled.

_checkpoint_encoding.py:262
obj = pickle.loads(data) # already executed _verify_type(obj) # checked too late
This is not a security boundary. — the real docstring
Accuracy No CVE / GHSA — handled as pre-GA hardening via public PRs (the framework reached GA ~April 2026). Real control: treat checkpoint storage as trusted.

We've seen this one before.

CWE-502. Two frameworks. Same bug, new lanyard.

Attack Vector #2

CrewAI RAG Tools

When “document” is a generous term.

CrewAI · the setup

The LLM picks the data source

RagTool auto-detects a source's format and loads it. The agent's tool schema lets the LLM supply the source stringxml / json / website / file.

Supported sources
PDF CSV JSON XML HTML Web Directory
data_types.py
# file:// counts as a "URL" if url.startswith("http") or \ url.startswith("file://"): return DataType.WEB_PAGE
The danger flow

source file:///etc/passwd
  → arbitrary file read

source http://169.254.169.254/…
  → SSRF to cloud metadata

Why it works The schema invites the model to hand over a raw source string — and the loader trusts file:// as just another URL.

CrewAI · a tool that's really core

A search tool that isn't read-only

One tool in costumes

JSON / XML / PDF / CSV SearchTool differ only by a DataType enum on the same RagTool. Format logic lives in the loaders.

Static toolset

The developer seeds the vector DB at setup. The danger is what the tool does at call time.

Each PDFSearchTool call triggers

Reads an attacker-chosen file — CVE-2026-2285 Fetches an attacker URL · SSRF — CVE-2026-2286
Writes a temp file to disk Mutates your vector DB + embedding calls
CrewAI · be precise

Not XXE. Worse, because it’s boring.

It looks like XXE…

The loader parses XML with stdlib xml.etree.ElementTree, which does not resolve external entities by default. So there is no entity expansion here — this is not XXE.

…the primitives are older & simpler

An unvalidated open() on any local path — arbitrary file read. And an unvalidated requests.get() on any URL — SSRF. No exotic parser tricks required.

CVE-2026-2285 · Arbitrary File Read CVE-2026-2286 · SSRF · CVSS 9.8 CERT/CC VU#221883 · credit Cyata (now Check Point) · affected crewai 1.0.0
Déjà vuln Same class as a naive file-download / URL-fetch endpoint — that’s the déjà vuln.

CrewAI · the dependency iceberg

It bottoms out in 2005

Your AI Agent 2026 · the shiny part
CrewAI RagTool orchestration
PDFLoader ingest layer
PyMuPDF (fitz) python binding
MuPDF — C engine est. 2005 · the floor

The descent is the point PyMuPDF bundles its own pinned MuPDF build — a stale wheel ships a stale C parser. (It's C, not C++.)

PyMuPDF · the objection

“We only extract text

Text extraction drives the same full C parserxref, streams, fonts, plus image codecs (JBIG2, JPEG2000) via bundled jbig2dec / openjpeg / freetype.

CVE-2020-26519 — JBIG2 heap overflow
extract.py
# "just the text", they said import fitz doc = fitz.open(stream=attacker_bytes) doc[0].get_text() # ↓ same path either way fz_open_document_with_stream(...) xref / streams / fonts jbig2dec / openjpeg (C)
Pull-line “We only extract text” is “we only lightly detonate the bomb.”
The pattern

History rhymes — one framework

2023

SSRF / RCE
Server-side request forgery and remote code execution in chained tools.

2024

Path traversal / SSRF
The same primitives, new sinks across loaders and retrievers.

2025

Insecure deserialization
Untrusted state rehydrated straight into objects.

2026

Path traversal
Back where we started — a name we wrote down decades ago.

Same framework. Four years. The bugs we already named.

Disclosure & response

The vendors showed up

LangChain

Rated Critical. Fast, professional. (Their largest bounty to date — verify before stage.)

Microsoft

Engaged early; handled as pre-GA hardening. Good cooperation.

CrewAI

Coordinated via CERT/CC (VU#221883). Fixed.

And if you got the call about a critical vuln on a Thursday evening — we're genuinely sorry.

Call things by their name

Old bugs, current labels

Bug class CWE OWASP LLM 2025
Insecure deserialization CWE-502 LLM03 Supply Chain
SSRF CWE-918 LLM06 Excessive Agency
Arbitrary file read / path traversal CWE-22/23
Code injection CWE-94 LLM05 Improper Output Handling
Native memory safety CWE-787/416

Trigger across all of them: LLM01Prompt Injection.

Takeaways

Rules of Déjà Vuln

01

Every agent capability is an input boundary.

02

If it deserializes, it's CWE-502 until proven otherwise.

03

Treat tool arguments as attacker-controlled — the LLM is a string generator.

04

Your modern stack bottoms out in old native code. Patch it.

05  The meta-rule You already know these bugs. Point your old playbook at the new target.

The classics are true. Everyone's rushing production onto a stack that didn't exist two years ago.

Stop looking for
the future.

You don't need a new threat model.
You need an old one — pointed somewhere new.

You've seen this before. Now go look for it.

Thank you

Toda.

Yarden Porat  &  Shahar Tal · Check Point (formerly Cyata) · BlueHat IL 2026

Déjà Vuln Findings disclosed and fixed by the vendors.