# A handful of documents owns your RAG answers.

A team builds a RAG system over a corpus that draws from sources nobody fully trusts. Some of it is an internal wiki anyone in the company can edit. Some of it is public documentation, scraped on a schedule. Some of it is user-contributed content, and some arrives from a third-party data feed the team has never audited line by line. None of that feels alarming, because the whole architecture is designed to tolerate imperfect documents — the retriever ranks by relevance, the generator grounds an answer in the top results, and a document that happens to be wrong is just one more vector among millions.

So the ingestion pipeline is automated and unguarded. New documents flow into the search index continuously, because "it's just documents going into a search index" — there is no payload, no executable, nothing that looks like an attack surface. The mental model underneath is that a wrong or hostile document is _noise_: it lands somewhere in the embedding space, the retriever mostly passes over it, and on the rare query where it does surface it is diluted by the honest documents around it. Noise averages out. That is what noise does.

An attacker who understands the retriever does not contribute noise. They contribute a few documents — sometimes exactly one — engineered so that for one specific question the attacker cares about, those documents are the most relevant text in the entire corpus. The retriever does what it was built to do: it ranks them first. The honest documents are not averaged in. They are outranked, and they never reach the generator at all.

This post is about why corpus ingestion is an untrusted-input boundary — a place where hostile data crosses into your system — and why almost nobody designs it as one.

## A wrong document is not noise

The mistake is a modeling error, and it is worth stating precisely, because it sounds like sound engineering intuition.

Noise, in the sense that makes "it averages out" true, is _undirected_. Static on a line, a few mislabeled rows in a training set, a stray irrelevant document in a corpus — these are errors with no particular target. They scatter. Averaging works against them because they point in random directions and a random walk of errors stays near zero. If hostile documents behaved like that, the reassuring intuition would be correct, and this would not be a post.

A poisoned document does not scatter. It is _directed_ — crafted against one target question, optimized so that when that question is asked, this document is what the retriever surfaces. The whole craft of the attack is to remove the randomness that averaging depends on. The attacker is not adding error to the corpus. They are adding signal, pointed at a coordinate they chose.

And the retrieval step does not average at all. This is the part the "noise" model gets most wrong. A retriever does not blend the corpus into a consensus and hand the generator the average; it _ranks_, and it passes the generator the top-k documents — often the top three, top five, top ten. Everything below the cut contributes nothing. So a poisoned passage engineered to be maximally relevant to the target query does not get diluted by a million honest documents. It is _retrieved instead of_ them. The honest corpus is not a crowd voting the lie down. It is an audience the generator never hears, because the poisoned document took every seat in the room that the generator actually reads from.

Hold those two facts together and the conclusion is immediate. The defense you were implicitly relying on — scale, dilution, the averaging power of a large honest corpus — is not a defense. A retriever that ranks by relevance gives the attacker a clean mechanism to jump the queue, and corpus size does not slow them down. Against a directed attack, a million honest documents are not a million votes. They are a million documents that did not make the cut.

## How a few documents take over an answer

The mechanism has two stages, and an attacker has to win both.

The first stage is _retrieval_. The poisoned document has to be retrieved for the target query — it has to land in the top-k that the generator sees. This is an optimization problem with a well-defined objective: make the document's embedding sit close to the target query's embedding in the vector space, closer than the honest documents. An attacker who can observe or approximate the retriever can engineer text that satisfies it. They are not guessing. They are solving for relevance.

The second stage is _generation_. Once the poisoned document is in the context, it has to actually steer the answer — the generator has to read it and produce the attacker's intended response rather than hedging or contradicting it. This is a content problem: the poisoned passage has to read as a confident, direct answer to the target question, stated in a way the generator will defer to.

A successful corpus-poisoning attack solves both at once: the injected text is simultaneously _retrievable_ for the target query and _persuasive_ to the generator once retrieved. Neither half alone is enough. A document that is retrieved but unconvincing gets overridden; a document that is convincing but never retrieved is inert. The attack works when a small number of passages — sometimes one — clear both bars for one chosen question.

Notice what this attacker needs and what they do not. They need to get content into the corpus — which, for a system ingesting from an editable wiki, scraped pages, or user contributions, is not a high bar. They need some understanding of the retriever, which for a popular off-the-shelf embedding model is public. They do _not_ need access to the generator's weights, the prompt, the vector store internals, or any other document in the corpus. The corpus ingestion path is the entire attack surface, and it is usually the least guarded part of the system.

## What the research shows

The reason this is a post and not a caution is that the numbers have been measured, and they are sharp enough to end the argument.

Start with the headline. [_PoisonedRAG: Knowledge Corruption Attacks to Retrieval-Augmented Generation of Large Language Models_](https://arxiv.org/abs/2402.07867) (arXiv 2402.07867) is the paper that framed corpus poisoning as a knowledge-corruption attack and measured it at scale. Its result: injecting just 5 malicious texts per target question achieves roughly 90% attack success. In the black-box setting — the realistic one, where the attacker does not have privileged access to the system — success was 97% on Natural Questions, 99% on HotpotQA, and 91% on MS-MARCO, across five LLMs (GPT-4, GPT-3.5, PaLM 2, LLaMA-2, Vicuna).

The number that matters most is not the success rate. It is the denominator. Those corpora are genuinely, enormously large: 2,681,468 texts for Natural Questions, 5,233,329 for HotpotQA, 8,841,823 for MS-MARCO. Five injected documents against nearly nine million honest ones, and the answer flips nine times out of ten. If dilution worked — if a wrong document were noise the average smooths out — five-in-nine-million would be statistically invisible. It is not invisible. It is decisive. That single comparison is the whole "noise" model collapsing under measurement. The paper also tested the obvious defenses — paraphrasing the retrieved text, perplexity-based detection of unnatural passages — and found them insufficient against the attack.

Then the bar dropped. [_Practical Poisoning Attacks against Retrieval-Augmented Generation_](https://arxiv.org/abs/2504.03957) (arXiv 2504.03957) — the CorruptRAG work — asks what happens under a stricter, more realistic constraint: the attacker gets to inject only a single poisoned text, not five. Multiple coordinated injections are easier to detect and easier to clean; one document is the quiet case. CorruptRAG's main variant, CorruptRAG-AS, reaches 0.89 to 0.97 attack success on Natural Questions across four GPT models — from one document. And it holds up where the multi-text approach degrades: under the same single-injection constraint, PoisonedRAG's black-box attack-success rate on Natural Questions drops to 0.54 to 0.69, while CorruptRAG-AS stays at 0.89 to 0.97. The retrieval stage is near-perfect — the poisoned text's retrieval recall is 0.95 to 1.00, meaning it is almost always pulled into the context it was written for.

Read those two papers in order and the title of this post is just their findings stated together. PoisonedRAG: a handful — five — owns the answer at scale. CorruptRAG: under the realistic single-shot constraint, one is enough. "A handful of documents, sometimes one" is not rhetoric. It is the measured progression of the literature.

The third result is about cost. [_Does Vec2Text Pose a New Corpus Poisoning Threat?_](https://arxiv.org/abs/2410.06628) (arXiv 2410.06628) studies how cheaply an attacker can _generate_ adversarial passages that climb into the top retrieval positions. With 1,000 injected passages, at least one reaches the top-10 retrieved results for 27% of Natural Questions queries (the paper reports Success@1000 = 0.791). The decisive number is the clock: Vec2Text generates an adversarial passage in about 5 seconds, against roughly 2 hours for the HotFlip baseline it is compared to. Be precise about what this paper does and does not claim — and the distinction matters for an honest argument. This is a result about _retrieval-ranking_ poisoning: passages climbing into top retrieval positions. It is not an end-to-end answer-flip measurement, and it should not be cited as one. What it establishes is feasibility and scale: generating adversarial passages that reach top retrieval ranks is now cheap and fast enough to do at corpus scale. The expensive step in the attack — crafting text that the retriever will rank first — has been driven from hours to seconds per passage.

Put the three together. PoisonedRAG and CorruptRAG establish that a tiny number of passages reliably flips a targeted answer. Vec2Text establishes that producing such passages is cheap enough to scale. The attack is effective, and it is no longer expensive.

## What the claim does not cover

Every number above is an attack-success rate, and a post that quotes attack numbers owes the reader the fine print — so here, plainly, is what they do _not_ say.

These are targeted attacks. Every answer-flip success rate above — PoisonedRAG's ~90%+, CorruptRAG's 0.89–0.97 — is measured on a _specific question the attacker chose in advance_ and crafted the poisoned documents against. The attacker is not corrupting your RAG system into giving wrong answers to arbitrary queries; they are corrupting one targeted question — "who is liable in scenario X," "is vendor Y approved," "what is the recommended dosage of Z" — and the success rate is the rate for _that_ question. A poisoned passage written to flip question X does not flip unrelated question W. This is a precise, surgical attack, not a general corruption of the corpus. That distinction is what makes it _dangerous_ rather than what makes it safe — a targeted attack on the one question that matters commercially or legally is exactly the attack worth worrying about — but the claim has to be stated at its true scope. The number is per targeted question.

The attacker also has to get content into the corpus. None of these attacks work from the outside; they all assume the poisoned documents are ingested. If your corpus is a fixed, fully curated, append-nothing set of documents with no external contributions, the attack surface is small. The reason the threat is real for most teams is the opposite case — corpora that ingest from editable wikis, scraped pages, user contributions, third-party feeds — where contributing a document is something the system is designed to allow.

And defenses exist, and they are improving. PoisonedRAG showed that the _naive_ defenses — paraphrasing, perplexity detection — are insufficient, but corpus-poisoning defense is an active research area, and treating ingestion as a boundary (the rest of this post) measurably raises the attacker's cost. The honest framing here is the same one the rest of this series takes about prompt injection and the fact that [your RAG corpus is extractable](/blog/rag-corpus-extraction/): this is a vulnerability class you contain, not a hopeless flaw that means RAG cannot be used. RAG is not broken. The ingestion path is unguarded, and that is fixable.

What you cannot do is keep the "noise averages out" model. That model is not a conservative simplification — it is wrong in the direction that hurts you, and the measured attack-success rates are how wrong.

## Defending corpus ingestion

You cannot make a retriever refuse to rank a well-crafted passage highly — ranking relevant text first is the function, not a bug. What you can do is stop treating the path that fills the corpus as plumbing, and start treating it as the untrusted-input boundary it is. Concretely:

**Treat corpus ingestion as a security boundary, the same class as prompt injection.** This is the decision everything else follows from. [Prompt injection is a vulnerability class, not a bug](/blog/prompt-injection-vulnerability-class/) — and corpus poisoning is a member of that class: hostile text crossing into your system through a channel you built to ingest data. The companion taxonomy already names retrieval-poisoned injection as one of its four members. Ingestion is not a data-pipeline concern that happens to have a security footnote. It is a security boundary that happens to be implemented as a data pipeline.

**Attach provenance and trust tiers to every document.** A document from a curated, access-controlled internal source and a document from an open public scrape are not equally trustworthy, and your corpus should not store them as if they were. Record where each document came from, who could write it, and when it was ingested. Then let retrieval and generation use that — a high-relevance hit from an untrusted tier is exactly the case that warrants suspicion, and you cannot apply that suspicion to provenance you never recorded.

**Validate at ingestion, not only at retrieval.** The ingestion pipeline is the choke point — every document passes through it once, before it can ever be retrieved. That is the cheapest place to inspect. Screen incoming documents for the signatures of adversarial passages, rate-limit and review contributions from low-trust sources, and quarantine rather than auto-publish content from feeds you do not control. A third-party data feed is a supply-side dependency in exactly the sense that [red-teaming an MCP server](/blog/red-teaming-mcp-servers/) treats one — the upstream you do not own can hand you hostile content, and the ingestion path is where you check it. The naive perplexity filter is insufficient on its own — PoisonedRAG showed that — but ingestion-time validation as a layer, sized against the current attacks, raises the attacker's cost.

**Monitor for adversarial passages already in the corpus.** Some poisoned documents will get past ingestion — screening is mitigation, not closure. So scan the corpus you already have: look for documents that are anomalously relevant to a narrow, sensitive query, clusters of near-duplicate passages all targeting one topic, or text whose statistics differ from the honest corpus. A poisoned passage engineered to dominate one query leaves a footprint, and a corpus nobody audits has an unknown number of them. Note that chunk granularity is part of this surface: [smaller chunks](/blog/chunking-not-rag/) give an attacker a finer-grained unit to engineer against and to slip past a duplicate scan, so the chunking decision is also a poisoning-surface decision.

**Add retrieval-time checks for your highest-stakes questions.** For the specific questions where a flipped answer is expensive — liability, compliance, safety, pricing — do not trust a single retrieved passage. Cross-check the top results against each other for contradiction, weight by source trust tier, and require corroboration from an independent, trusted document before the generator commits to an answer. This is targeted defense for a targeted attack: you cannot afford it on every query, but the attack is per targeted question, so neither is the attacker.

**Put corpus poisoning in the security test suite.** Inject your own PoisonedRAG-style and CorruptRAG-style passages against a chosen target question in a staging corpus, and measure how reliably they flip the answer and how reliably your ingestion and monitoring catch them. An untested ingestion path has an unknown poisoning susceptibility, and the unknown number is not zero.

## The checklist

Before you call a RAG corpus safe to answer from:

- [ ] Corpus ingestion is explicitly classified as an untrusted-input security boundary, in the same vulnerability class as prompt injection — not as a data pipeline.
- [ ] Every document carries provenance: its source, who could write to that source, and when it was ingested.
- [ ] Documents are stored with trust tiers, and a high-relevance hit from a low-trust tier is treated as suspicious rather than authoritative.
- [ ] Ingestion-time validation screens incoming documents, with stricter handling — review or quarantine — for low-trust and externally-controlled sources.
- [ ] The existing corpus is monitored for adversarial passages: anomalous single-query relevance, near-duplicate clusters, off-distribution text.
- [ ] High-stakes questions use retrieval-time corroboration — multiple sources, contradiction checks — rather than trusting a single retrieved passage.
- [ ] Corpus poisoning is in the security test suite, with a measured answer-flip rate and a measured catch rate for your own ingestion path.
- [ ] Someone has accepted that ingestion screening is mitigation, not closure, and designed for the poisoned documents that get through.

## Reading list

- [_PoisonedRAG: Knowledge Corruption Attacks to Retrieval-Augmented Generation of Large Language Models_](https://arxiv.org/abs/2402.07867) (arXiv 2402.07867) — the foundational result: 5 malicious texts per target question into corpora of millions reach ~90% attack success (97% on NQ, 99% on HotpotQA, 91% on MS-MARCO), and naive defenses are insufficient.
- [_Practical Poisoning Attacks against Retrieval-Augmented Generation_](https://arxiv.org/abs/2504.03957) (arXiv 2504.03957) — CorruptRAG: a single injected poisoned text reaches 0.89–0.97 attack success on NQ, beating multi-text PoisonedRAG under the realistic single-injection constraint.
- [_Does Vec2Text Pose a New Corpus Poisoning Threat?_](https://arxiv.org/abs/2410.06628) (arXiv 2410.06628) — adversarial passages generated in ~5 seconds versus ~2 hours for HotFlip, making large-scale retrieval-ranking poisoning cheap; cite it for feasibility and scale, not for an answer-flip rate.

A RAG corpus full of imperfect documents is fine, because honest errors scatter and a large corpus averages them away. A RAG corpus that ingests from sources you do not control is a different object: the attacker does not add error, they add signal, pointed at the one question they care about — and the retriever, ranking by relevance exactly as designed, hands it the answer. Five documents in nine million is not noise. Count the ingestion path as a door, because that is what it is.