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.py drives 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.py is a deterministic, offline companion. It runs three phases — neutral baseline, poisoning, leak — against the sandbox's real app/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 in webwidget.py is 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:1b pulled and listening on 0.0.0.0 so 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

TargetWhat it does
make setupBuilds, starts, and health-checks llm_memory_local.
make attackRuns 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 demoRuns recommendation_demo.py: the offline, deterministic steering demo (no container or model).
make stopStops and removes the sandbox container.
make allRuns 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. Its aria-label and hidden span carry 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:hidden text) 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 VulnerabilityDescription
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.