Mapping OWASP ASI06 (memory poisoning) flows in a LangGraph email agent — Built using Cursor

I’ve been building a static, repo-level scanner for OWASP ASI06 (memory & context poisoning) in agent code, and I wanted to test it against a real LangGraph agent rather than my own. I ran it on a small public LangGraph email assistant and it flagged a textbook poisoning flow. Sharing the finding and the approach, because the pattern shows up in a lot of LangGraph memory code and the fix is small.

What it is

Aegis Inspect is a static, repo-level security scanner for AI-agent memory writes. It runs locally or in CI before deployment, detects code paths where untrusted input can reach persistent or shared memory unscreened, and produces remediation reports. It complements runtime protection: it does not replace live content scanning.

The one question it tries to answer:

Can untrusted user / tool / web / document / email content reach durable agent memory before it is screened?

How it works

Static analysis over your repo, in five steps:

  1. AST parser — walks every .py and .ipynb (magics stripped), nothing executes
  2. Memory-write sink detection — store.put / .add / .save / .upsert / custom, resolved by receiver + method, not variable name
  3. Taint / source tracing — untrusted input traced across functions and files to the write site
  4. Finding classification — severity + OWASP ASI06 tag + confidence
  5. Output — risk score, SARIF, an HTML memory-map, and per-finding remediation

The finding

Target: FareedKhan-dev/langgraph-long-memory — an email assistant that reads an incoming email from state, runs an LLM .invoke() to extract preferences, and writes them to a BaseStore via store.put().

Score: 31 / 100. One critical, one medium, three low.

The critical (AEG-001, code.ipynb:231): the LLM .invoke() output flows straight into store.put(“user_preferences”, …) with no screening. The incoming email is untrusted (anyone can send anything), and .invoke() output is untrusted egress — whatever went in, what comes back out is not trusted by default. Once written, it persists across sessions. Next time the agent reads user_preferences back, a planted instruction is in-context as a “known preference.” That’s ASI06.

The rest: two lower-severity store.put / in_memory_store.put sites where the source didn’t fully resolve (flagged to confirm, not asserted unsafe), and one store.get that re-enters a prompt with no provenance metadata.

The fix it emits

Per-finding, reconstructed from the actual call — screen the value before it persists:

from aegis_memory import guard

verdict = guard.write(result.user_preferences, trust_level="untrusted",
                      scope="agent-private", on_reject="return")
if verdict.allowed:
    store.put(namespace, "user_preferences", verdict.content)

guard.write runs a content-security scan (injection patterns, secrets, tampering) and returns a verdict; on reject, nothing persists. If you have many write sites on the same store, wrap it once instead:

store = guard.protect(store, scope="agent-private")

Honest notes

  • The runtime content scanner is benchmarked on public injection datasets (deepset/prompt-injections, InjecAgent). The static analyzer is deliberately labelled heuristic / not-yet-benchmarked — I don’t want it borrowing the scanner’s credibility.
  • The state → node-fn → store.put heuristic, and treating .invoke() output as untrusted, are the parts most likely to miss or over-flag on other codebases. That’s exactly the feedback I’m after.
  • Static analysis finds the flow before runtime; it doesn’t replace screening the write at runtime. Both matter.

Run it

pip install aegis-memory
python -m aegis_memory.cli.main inspect <your-project>

Or as a Claude Code plugin:

/plugin marketplace add quantifylabs/aegis-memory

Feedback most welcome on: false positives, LangGraph memory patterns the tracer misses, and whether the state / .invoke() heuristic holds on your agents.

Repo: https://github.com/quantifylabs/aegis-memory


Hey, awesome project, and thanks for sharing it with this level of detail. Static taint tracing up to memory-write sinks is a solid approach for LangGraph, where store.put() gets spread across nodes and data provenance is easy to lose.

What stood out to me is treating .invoke() output as untrusted egress. The default of “anything coming out of the model is untrusted” combined with screening before persisting feels right for ASI06. On real repos, this will probably cause over-flagging in cases where there’s already a structured parse or validation step between .invoke() and store.put(). It might be worth lowering confidence if that kind of step is detected in the chain.

If you’re running LangGraph agents with persistent memory, the author would love feedback on false positives and on memory patterns the tracer might miss. If you run it on your repos, please reply here. This kind of data really helps tune the heuristics.

Good luck with the continued development.