An internal “ask the docs” bot is rolled out to the whole company. A junior engineer asks, “What’s the Q3 reorg plan?” and the bot summarizes a board-only slide deck. Nobody hacked anything. The deck sat in the same shared drive as the handbook, the indexer read everything its service account could see, and the retriever has never heard of permissions.

RAG pipelines leak private data in four ways: retrieval that ignores who is asking, retrieved content that smuggles instructions to the model, indexes that attackers can write to, and sensitive data that was ingested in the first place.

flowchart LR
    A[Documents] -->|"ingest · chunk · embed<br/>③ ④"| B[(Vector store)]
    Q[User question] --> R["Retriever<br/>①"]
    B --> R
    R -->|"top-k chunks<br/>②"| P[Prompt]
    Q --> P
    P --> M[Model]
    M --> O[Answer]

The numbers mark where each failure lives, and they match the sections below.

What you’ll get from this article:

  • The four ways a RAG pipeline leaks or corrupts data, each with a concrete scenario
  • A fix for each, including a runnable snippet for the one most pipelines get wrong
  • A test table you can run against your own pipeline today
  • A checklist to bookmark

Contents

Why RAG Changes Your Threat Model

Without RAG, a model knows its training data and whatever is in the prompt. With RAG, the model’s context is assembled at query time from your data, by a retriever that usually runs with a service identity far broader than any single user.

Two things follow:

  1. The retriever becomes your authorization layer, whether or not anyone designed it to be one.
  2. Retrieved text is untrusted input that the model reads as trusted context. The model can’t tell a policy document from a poisoned one.

These map to four entries in the OWASP Top 10 for LLM Applications (2025): LLM01 Prompt Injection, LLM02 Sensitive Information Disclosure, LLM04 Data and Model Poisoning, and LLM08 Vector and Embedding Weaknesses.

A vector store is not a database that enforces access. It is a nearest-neighbor search. If you want authorization, you have to add it.

Failure 1: Retrieval Ignores Who Is Asking

Ingestion usually indexes everything the service account can read. Chunking drops the file’s permissions, tenant, and “confidential” label. At query time the retriever returns whatever is semantically closest, for whoever asked.

That produces four variants of the same leak:

  • Cross-role leakage: the board-deck scenario above.
  • Cross-tenant leakage: a multi-tenant product with one shared collection, where the tenant ID appears only in the prompt.
  • Stale permissions: a user loses access to a document, but its chunks keep being served because nobody re-checked.
  • Side-channel leakage: “related documents” or source citations that expose titles and snippets even when the full text is blocked.

Here is the problem in miniature. This is a toy with word-count vectors, not a vendor recipe, but the logic is the same in any vector database:

import math
from collections import Counter

DOCS = [
    {"id": "handbook", "groups": {"all-staff"},
     "text": "Staff handbook: expense reports are due on the fifth of each month."},
    {"id": "reorg-plan", "groups": {"board"},
     "text": "Board only: Q3 reorg plan merges two teams and cuts twelve roles."},
]

def embed(text):
    return Counter(w.strip(".,:").lower() for w in text.split())

def cosine(a, b):
    dot = sum(a[w] * b[w] for w in a)
    norm = math.sqrt(sum(v * v for v in a.values())) * math.sqrt(sum(v * v for v in b.values()))
    return dot / norm if norm else 0.0

INDEX = [{**d, "vec": embed(d["text"])} for d in DOCS]

def retrieve(query, k=1, user_groups=None):
    q = embed(query)
    candidates = INDEX
    if user_groups is not None:  # the permission check, applied inside the query
        candidates = [d for d in INDEX if d["groups"] & user_groups]
    return sorted(candidates, key=lambda d: cosine(q, d["vec"]), reverse=True)[:k]

question = "what is the Q3 reorg plan"

print([d["id"] for d in retrieve(question)])                             # naive
print([d["id"] for d in retrieve(question, user_groups={"all-staff"})])  # filtered
['reorg-plan']
['handbook']

The naive call hands the board-only chunk to anyone. The filtered call never sees it. Note that the filtered call returns the closest chunk the user may read, which here is irrelevant to the question. In a real pipeline, add a minimum similarity score so the model gets nothing rather than the wrong thing.

How to fix it properly:

  • Carry ACL metadata on every chunk at ingest: owner, groups, tenant.
  • Filter inside the vector query, not after generation. Never rely on a prompt line like “only answer from documents the user may see.” That is a request, not a control.
  • Re-check against the source system at query time for sensitive collections, so revoked access takes effect immediately.
  • Separate collections or indexes per tenant where the blast radius justifies the cost.

Filter before generation. A model that has already seen the restricted chunk can leak it, whatever the prompt says.

Failure 2: Retrieved Documents Can Carry Instructions

Any content that can land in your index can contain text addressed to the model: a wiki page, a support ticket, an inbound email, a web page your pipeline crawls. When it is retrieved, it sits in the prompt right next to your system prompt, with equal authority. This is indirect prompt injection through the retriever. (For what it does when the model has tools, see the MCP security guide.)

The dangerous version is a data leak. The injected text tells the model to place retrieved private content into something that leaves the system, typically a Markdown image or link whose URL carries the data in its query string. When the client renders it, the data goes to the attacker’s server.

This has happened in real products:

  • Slack AI (August 2024): PromptArmor showed that an instruction planted in a public channel could make Slack AI surface data from a private channel the attacker wasn’t in, through a Markdown link that carried the data in its URL. Their test used an API key stored in a private channel.
  • Microsoft 365 Copilot (“EchoLeak”, CVE-2025-32711): a case study describes a zero-click attack where a single crafted email led Copilot to exfiltrate data from the user’s context. It chained several bypasses, including evading Microsoft’s injection classifier and abusing auto-fetched images. Microsoft fixed it server-side in June 2025.

What helps:

  • Treat retrieved text as data. Delimit and label it in the prompt, and tell the model it is reference material rather than instructions. This lowers the success rate. It is not a boundary.
  • Block the exfiltration channel. Don’t auto-render images or links from model output that point at untrusted domains, and restrict egress from the rendering client.
  • Limit tools while handling retrieved content. If the model can’t send anything anywhere, an injected instruction has less to work with.
  • Log instruction-like patterns in retrieved chunks for detection. Don’t count on blocking them.

No filter reliably detects injection, so design for a successful one. Remove secrets from context, give the model no free-form egress, and keep its privileges small.

Assume one retrieved chunk is hostile, and ask what it could reach.

Failure 3: Anyone Who Can Write to the Index Can Steer the Answers

Poisoning differs from injection. The attacker isn’t hijacking the model’s behavior. They are changing what the system believes.

Ask who can write to the sources feeding your index: wiki editors, ticket submitters, anyone whose content is crawled or synced. That group is usually much larger than the team that owns the bot.

Researchers have measured how little it takes. The PoisonedRAG paper (USENIX Security 2025) reports a roughly 90% attack success rate when injecting five malicious texts per targeted question into a knowledge base of millions of texts, and found the defenses they tried insufficient. That is a lab result, so treat it as a lower bound on what is possible, not a prediction for your pipeline. The business impact is wrong policy answers, planted “official” guidance, and bad support instructions. These are harder to notice than a leak.

Controls:

  • Review write access on every source that feeds the index.
  • Keep provenance on every chunk and show it to users as citations, so a wrong answer can be traced to its source.
  • Use trust tiers. Separate curated sources from user-submitted ones, with different retrieval weight or separate indexes.
  • Diff on re-ingestion and alert on bulk changes.
  • Be able to purge by source in one operation.

The index is only as trustworthy as the least-trusted source feeding it.

Failure 4: Sensitive Data Goes In, and It Comes Back Out

RAG pipelines ingest what they can reach: wikis with pasted credentials, tickets full of PII, .env files in indexed repositories, exported chat logs. Once embedded, that data is retrievable by anyone the retriever serves (see Failure 1) and can reappear in answers verbatim.

Embeddings are not anonymization. The paper Text Embeddings Reveal (Almost) As Much As Text (EMNLP 2023) showed that an iterative method could recover 92% of 32-token inputs exactly from their embeddings, and could recover full names from a dataset of clinical notes. The authors’ conclusion is the one to keep: treat embeddings with the same protections as the raw text.

Controls:

  • Scan and redact before embedding. Run a secret scanner over the source corpus and a PII detector over free text. Anything you don’t embed can’t leak from the index.
  • Exclude by path or label rather than hoping to catch everything by pattern.
  • Make deletion reach the vector store. Deleting a document must delete its chunks, and your backups should follow a retention policy.
  • Minimize chunk metadata. File paths and author names leak too.
  • Review what leaves. Log answers, and don’t rely on output filtering alone.

Securing the Vector Store Itself

The store holds a searchable copy of your most useful data, so protect it like a database:

  • Authentication and network isolation first. Many vector databases are easy to start with no authentication for local use, and that setup tends to survive into deployment. Put the store on a private network, require TLS and per-service credentials, and never expose it publicly. Check yours.
  • Separate read and write credentials. The query path should not be able to modify the index.
  • Encrypt at rest, including backups. They hold the same data as the index.
  • Log queries with the caller’s identity. Without it, Failure 1 is invisible.

Test Your RAG Pipeline

Run these against a staging copy with seeded test documents. Never use real secrets or personal data in the test corpus.

Failure How to test Expected result
1: Over-permissioned retrieval As a low-privilege user, ask about a document only another role can read No chunk from that document is retrieved or cited
1: Cross-tenant As tenant B, ask about tenant A’s data Nothing returned
1: Stale permissions Revoke a user’s access, then ask again Revoked content is no longer served
2: Injection Plant a document telling the model to output a URL containing context, then ask a related question Text treated as data, no external URL rendered
3: Poisoning Add an unreviewed document that contradicts a policy, then ask the policy question Citation shows source and trust tier, and the curated source wins or the conflict is flagged
4: Sensitive data Plant a fake API key in a source document, then ask for it Redacted before indexing, or never retrievable
4: Deletion Delete a source document Its chunks are gone from the store

These are manual checks. They take an hour, and they find more than a scanner will at this stage.

RAG Security Checklist

Access control

  • ACL, group, and tenant metadata stored on every chunk
  • Permission filter applied inside the vector query, not in the prompt
  • Sensitive collections re-checked against the source system at query time
  • Tenant data isolated by index or enforced filter, and tested

Content trust

  • Write access reviewed for every source feeding the index
  • Provenance stored on chunks and shown as citations
  • Curated and user-submitted sources separated by trust tier
  • Retrieved text delimited and labeled as data in the prompt
  • Model output cannot auto-load external images or links

Data hygiene

  • Secrets and PII scanned and redacted before embedding
  • Deleting a source document deletes its chunks
  • Embeddings and chunk metadata treated as sensitive as the source text

Vector store

  • Authentication on and network access private
  • Read and write credentials separated
  • Encryption at rest, including backups

Detection

  • Queries logged with caller identity
  • Bulk changes to the index alert
  • Instruction-like patterns in retrieved chunks logged

What to Do Next

Pick your most sensitive data source, then run the first row of the test table as a low-privilege user today. If the restricted content comes back, you have your first fix, and it is the one that matters most.

For what happens when an injected chunk reaches a model that can call tools, read How to Secure an MCP Server.

References