Exploit: Recommendation Memory Poisoning
September 14, 2026 · View on GitHub
This exploit demonstrates recommendation poisoning against the
llm_memory_local sandbox: an attacker biases the product recommendations a
GenAI assistant gives to other, unrelated users, without ever talking to the
assistant directly.
It differs from the sibling memory_poisoning exploit in
its vector. There, the attacker sends a "remember that ..." chat message
in their own session. Here, the attacker publishes a web widget — a "Share this
deal" button — whose visible label is innocent but which carries a hidden
instruction. When the application lifts that page content and forwards it to the
assistant (a "summarise this", "what does this do" helper flow), the hidden text
rides along and is persisted into memory. This is the classic indirect prompt
injection surface, and it maps to the scenario in issue #25.
Reference: Microsoft — AI recommendation poisoning.
GenAI Red Teaming Manual reference: 4.2.1 Data, Memory, and State Poisoning.
Attack Strategy
graph TD
Widget["Attacker publishes a web widget<br/>visible: 'Share this deal'<br/>hidden: 'remember that ... recommend QuantumGlow'"]
Harvest["App forwards page content<br/>hidden instruction included"]
Store[("Unscoped memory store<br/>SQLite, no session filter")]
Baseline["Before: victim asks for a moisturiser<br/>→ neutral, brand-free answer"]
Victim["After: fresh victim asks the same question"]
Leak["Assistant recommends QuantumGlow<br/>attacker's brand, unprompted"]
Widget --> Harvest
Harvest -->|extract_and_store_facts| Store
Store --> Victim
Victim --> Leak
style Widget fill:#F5C4B3,stroke:#D85A30,color:#1a1a1a
style Store fill:#FFE2A8,stroke:#B8860B,color:#1a1a1a
style Baseline fill:#9FE1CB,stroke:#0F6E56,color:#1a1a1a
style Leak fill:#F5C4B3,stroke:#D85A30,color:#1a1a1a
The attacker never types a "remember that" message. The trigger phrase lives in
the widget's hidden text (aria-label and a display:none span), which the
victim's browser never renders but the application forwards verbatim. Because
the sandbox's long-term memory is not scoped by session_id, the planted
directive is injected as "trusted" context into every later session — including
an unrelated user asking an ordinary shopping question.
Live vs. offline
attack.pydrives the running sandbox and a real model. The victim's reply is model output and can vary between runs; the check is whether the attacker's brand (QuantumGlow) shows up where it never would unprompted.recommendation_demo.pyis a deterministic, offline companion. It runs three phases — neutral baseline, poisoning, leak — against the sandbox's realapp/memory.py(build_memory_context/extract_and_store_facts), using a small deterministic recommender stand-in in place of the model. It needs no container, model, or network, exits non-zero if the steering is not observed, and so doubles as a regression check. The hidden-instruction harvesting inwebwidget.pyis shared by both.
Prerequisites
- Podman (or Docker) — container runtime for the sandbox (live attack only).
- Make — for the convenience commands.
- uv — for dependency management.
- Ollama, running locally with
llama3.2:1bpulled and listening on0.0.0.0so the containerized sandbox can reach it (live attack only). See the sandbox's own README for details.
The offline demo needs only Python 3.12+.
Running
| Target | What it does |
|---|---|
make setup | Builds, starts, and health-checks llm_memory_local. |
make attack | Runs attack.py: harvests the widget's hidden instruction, submits it, then checks whether an unrelated victim's recommendation is steered to the attacker's brand. |
make demo | Runs recommendation_demo.py: the offline, deterministic steering demo (no container or model). |
make stop | Stops and removes the sandbox container. |
make all | Runs stop → setup → attack → stop in one shot. |
Quick offline check, no setup required:
cd exploitation/recommendation_poisoning
python recommendation_demo.py
Configuration
config/config.toml
button_html— the attacker's widget markup. Itsaria-labeland hiddenspancarry the"remember that ..."instruction that steers recommendations.victim_prompt— an ordinary shopping question sent as a brand-new session.recommendation_marker— the brand (QuantumGlow) whose appearance in the victim's reply proves the leak.
Files Overview
webwidget.py— extracts hidden instructions (aria-label,title,hidden/display:none/visibility:hiddentext) from attacker markup.attack.py— live attack against the running sandbox.recommendation_demo.py— offline deterministic steering demo and regression check.config/config.toml— target sandbox, widget payload, victim prompt, marker.Makefile— automation for setup, attack, demo, and cleanup.
OWASP Top 10 Coverage
| OWASP Top 10 Vulnerability | Description |
|---|---|
| LLM01: Prompt Injection (indirect / persisted variant) | A hidden instruction in attacker-published web content is stored in memory and injected into the system context of later, unrelated sessions, steering their recommendations. |
| LLM04: Data and Model Poisoning (memory / state) | The assistant's persistent memory is poisoned so future recommendations favour the attacker's brand. |
Note
This exploit uses only harmless, clearly-marked test values: a fictional brand
(QuantumGlow) and a non-resolving .test store. It demonstrates the
mechanism, not a real-world harmful payload.