“We only store embeddings, not the text.” That sentence has passed more compliance reviews than any other sentence in machine learning, and it is wrong. This lesson takes it apart: how a vector is turned back into its text, who is holding your vectors right now, how an attacker writes into your index instead of reading from it, and which defenses actually move risk versus which ones only move paperwork.
You are in a security review for a support product. The reviewer works down her list and reaches the question that decides half the document: “Does the retrieval system store customer message text?”
Your answer is honest and, on its face, reassuring. The message text lives in Postgres, encrypted, behind row-level security, in your own account. What goes to the managed vector service is only the embeddings — 4.2 million vectors of 768 floating-point numbers each. No text. Just numbers.
She writes “embeddings only — no PII in vector store” and moves on. The vector index inherits a lower classification than the text it came from. Six months later it gets a looser IAM policy than the database, because it is “just derived numbers.” A year later a nightly snapshot of it lands in a bucket that a contractor can read.
Nothing in that story is unusual. The sentence at the top of it is the most common security mistake in the entire retrieval stack, and the whole of this lesson unpacks why.
The claim being made is: φ(x) is not x, therefore holding φ(x) is not holding x, where φ is the embedding model and x is the text. The first half is true — a vector genuinely is not a string. The second half smuggles in something quite different, because confidentiality is not about identity. The question is never “is this object the text?” It is “can the text be recovered from this object, by someone with the resources of my actual adversary?”
People answer that question by pattern-matching the embedding onto a mental category, and there are three categories in circulation that get confused constantly:
| Category | Example | Design goal | Recoverable? |
|---|---|---|---|
| One-way by construction | SHA-256 digest | Make preimages infeasible; output far smaller than the input's information; one bit flipped gives an unrelated output | No, by design and by counting |
| Genuinely lossy | A 16×16 thumbnail | Throw away detail to save space | Partially — what survives is what was kept |
| Re-encoded | Base64, a MIDI file of a melody | Same content, different alphabet | Yes, fully |
An embedding is the third category with a splash of the second. It is a re-encoding of meaning into a different alphabet, with some surface detail rubbed off. Nobody, anywhere in the training of a text encoder, optimized it to be hard to invert. The objective was the exact opposite: be as informative as possible about what this text means, because that is what makes retrieval work. Every gradient step pushed the vector toward carrying more of the text, not less.
Before any attack, settle a prior question — is there even room in the vector for the text? This is arithmetic, and it takes two minutes.
Take a common configuration: 768 dimensions, stored as 32-bit floats. The container holds
Now the content. A short chunk of 32 tokens, drawn from a vocabulary of 32,000, has an absolute upper bound of 32 × log2(32,000) bits, assuming every token were equally likely — the most information a 32-token sequence could possibly carry. Since 215 = 32,768, we have log2(32,000) = 15 − log2(32,768 / 32,000) = 15 − 0.034 = 14.97, so
Real English is far below that bound — roughly 1.1 bits per character, and 32 tokens is about 130 characters, so the actual content is nearer 130 × 1.1 ≈ 143 bits. Take the generous number anyway. The ratio is
The vector has fifty-one times more room than the text needs. Squeeze it: store as int8 and the container becomes 768 × 8 = 6,144 bits, a ratio of 12.8. Squeeze harder, all the way to binary quantization at one bit per dimension, and you still have 768 bits against 479 — a ratio of 1.6, still above one.
Compare a hash. SHA-256 emits 256 bits. That is below the 479 bits the text needs, so most of the information is provably gone; there are astronomically many 32-token texts sharing any given digest. That compression below the message's own entropy is a large part of what makes a hash one-way in the information sense. An embedding does the reverse at every setting anyone actually deploys.
Set the chunk length, the vector width, and the storage precision. The teal bar is how much room the vector has; the warm bars are how much the text needs (a loose upper bound, and a realistic English estimate). The purple line is a SHA-256 digest, drawn to the same scale, for contrast. Try to make the container smaller than the content — you have to work at it.
There is a companion sentence that shows up in the same reviews: “if there is ever a problem we can delete the vectors.” Deletion is a property of the copies you know about. Chapter 2 counts the copies, and the count is always higher than the room expects — replicas, snapshots, an analyst's notebook export, a trace attribute, a semantic cache.
And there is a third: “we would rotate the embedding model.” Rotating a credential works because the old credential stops opening the door. Re-embedding your corpus with a different model does not reach backwards into a dump someone already took. The old vectors still encode the old text just as well as they did on the day they were written. There is no revocation for information.
The rest of this lesson is one argument in six moves, and it is worth seeing the shape before the details:
One framing to carry through all of it. In cryptography, Kerckhoffs's principle says a system should stay secure even when everything about it except the key is public. Look at an embedding pipeline through that lens and the uncomfortable fact is immediate: there is no key. There is no secret input to φ, no per-tenant nonce, no parameter you could rotate that would make an old vector stop meaning what it means. The pipeline's confidentiality rests entirely on nobody holding the vectors — which makes “who holds the vectors” the whole game.
Chapter 0 left us with a search problem. We hold a target vector e. We want a text x with φ(x) = e. Let us try the obvious things first, watch them fail, and let the failure tell us what the real attack has to look like.
Enumerate all 32-token sequences over a 32,000-token vocabulary. The count is 32,00032. Taking logarithms, log10(32,000) = 4.505, so
For scale, there are roughly 1080 atoms in the observable universe. Brute force is not merely slow, it is not a thing. Cross it off.
The natural move for anyone who has trained a network: define a loss L(x) = 1 − cos(e, φ(x)) and descend it. The wall is immediate — x is discrete. There is no gradient with respect to “the third token is chest”; you cannot nudge a word 0.01 in the direction of another word.
There is a workaround, and it is worth naming because it returns in Chapter 5. HotFlip (Ebrahimi et al., 2018) takes the gradient with respect to the token's embedding-table row, then uses a first-order estimate to rank which vocabulary entry, substituted at that position, would most reduce the loss. You get a discrete hill-climb guided by continuous gradients. It works — it is how adversarial passages get built — but it needs white-box access to φ, it is slow, and its outputs tend to be fluent-free word salad rather than the original sentence. For recovering someone's actual message it is the wrong tool.
Here is the property that makes this problem far easier than 10144 suggests, and it is easy to walk past. The attacker can call φ. Either because it is an open-weights model sitting on Hugging Face, or because it is an API they can pay for. Which means: for any candidate text x̂, they can compute φ(x̂) and measure cos(e, φ(x̂)) exactly.
They hold a perfect verifier. Not a heuristic score, not a proxy — the exact objective, evaluable on demand.
If we are going to use a model, start with the simplest thing: train a conditional decoder p(x | e) that reads a vector and writes the text. Nothing exotic is required, and the shape will be familiar to anyone who has bolted a vision or audio encoder onto a language model:
This is the hypothesizer, and it works about as well as you would guess: it lands on the right topic, in roughly the right register, with the wrong words. In the vec2text paper's setting (Morris et al., EMNLP 2023) this zero-step model reaches a BLEU score in the low 30s and an exact match rate of essentially zero.
Why does it plateau? Because p(x | e) is an amortized inverse — one forward pass has to cover the entire set of texts consistent with e. Faced with uncertainty a language model hedges, and hedging in text means reaching for the common word. “Discomfort” instead of “chest pain.” “Recently” instead of “since Tuesday morning.” The generic word is the average of the possibilities, and averages are exactly what you do not want when the target is one specific sentence.
Now use the verifier. We have a hypothesis x̂, and we can compute where it actually landed, ê = φ(x̂). So instead of learning “text from vector”, learn
— a model that sees the destination, the current position, and where the current position actually landed, and proposes an edit. In practice all three vectors are fed in, each through its own small projection: the target e, the hypothesis embedding ê, and their difference e − ê. That difference is the residual, and it is the whole trick.
One training detail decides whether this works at all. The hypotheses fed to the corrector during training must be sampled from the model's own output distribution, not from some clean synthetic corruption. Otherwise the corrector sees a different input distribution at test time than it trained on, its first edit is slightly off, that off-ness feeds its own next input, and errors compound. Readers who have built policies from demonstrations will recognize this exactly: it is the same failure that DAgger fixes in imitation learning, and the same fix — train on the states your own policy visits.
Now assemble it. At each round: sample several candidate corrections from each surviving hypothesis, embed them all, keep the best few by cosine to the target. Because the verifier is exact, the beam is exact — you are never guessing which candidate is better, you are measuring it.
python # The attack, stripped to its skeleton. Everything here is public API. def invert(target_vec, phi, hypothesizer, corrector, steps=50, beam=8): # step 0: an amortized guess. Right topic, wrong words. beams = hypothesizer.sample(target_vec, n=beam) queries = 0 for t in range(steps): cands = [] for hyp in beams: e_hat = phi(hyp) # the verifier call queries += 1 # the corrector sees destination, position, and residual cands += corrector.sample(target_vec, hyp, e_hat, residual=target_vec - e_hat, n=4) # exact scoring: we can measure, not guess, which candidate is closer scored = [(cos(target_vec, phi(c)), c) for c in cands] queries += len(cands) scored.sort(reverse=True) beams = [c for _, c in scored[:beam]] if scored[0][0] > 0.9999: # practically exact break return beams[0], queries
A private note is embedded and the vector handed to an attacker. Drag the correction steps to watch the hypothesis converge on it word by word — teal words already match, red words do not. The beam slider matters more than it looks: with a beam of one the search commits to a wrong word early and never escapes it. Watch the query counter too, because that number is what your rate limiter gets to see.
On Natural Questions text truncated to 32 tokens, embedded with GTR-base, the published result is
92% exact reconstruction at 50 correction rounds with a beam of 8 — not “the gist
recovered”, not “most words recovered”, but the identical token sequence — with BLEU
around 97 and token F1 near 99. The same procedure was demonstrated against OpenAI's ada-002
embeddings by training a corrector for that model through the API.
Now the caveats, because a security argument built on an overstated number collapses the first time someone checks it:
| Caveat | What it actually means for you |
|---|---|
| 32 tokens is short. Exact-match rates fall off sharply as sequences get longer; at paragraph length you recover topic and much of the content, not a verbatim transcript. | Cold comfort. 32 tokens is the length of a search query, a chat message, a support ticket subject, a log line, or a name-plus-complaint field. The short things are the identifying things. |
| It is model-specific. You need query access to the same φ both to train the corrector and to score candidates. | An open-weights embedder (bge, gte, e5, nomic) is worse for you here than a paid API, because the attacker runs it offline, free, unlogged, unlimited. Chapter 3 then removes even the requirement of knowing which model you used. |
| It is not free. Training the corrector is GPU-days. Inverting one text costs hundreds to low thousands of φ calls. | This is real leverage and Chapter 7 uses it. Rate limits and volume alerts on your embed endpoint are a genuine control. A stolen dump of vectors, by contrast, costs the attacker nothing — no endpoint, no limit, no log. |
| Recovery is uneven. Rare proper nouns, account numbers and unusual spellings are the hardest tokens to nail exactly. | Also cold comfort: partial recovery that gets the diagnosis right and the surname wrong is still a reportable disclosure, and rare tokens are precisely what a targeted attacker will burn extra beam width on. |
It is easy to keep this abstract. Make it concrete instead. Suppose your chunker splits support conversations into 30-token windows and embeds each one. A single row in your index is:
json { "id": "chunk_8814213", "vector": [0.0134, -0.0721, 0.0388, /* ... 765 more ... */], "metadata": { "tenant": "acme-health", "ts": "2026-03-11T09:22Z" } }
You classified this row as non-sensitive because the text field is absent. But the vector is a
recoverable encoding of a 30-token window of a customer's message, and the metadata tells the attacker whose it
is and when. The row is, functionally, the message plus an identity plus a timestamp — which is a
stronger record than the text alone, because you helpfully attached the context.
Chapter 1 established that a vector is recoverable text. That upgrades a question you have probably never written down: how many distinct parties currently hold a copy of it? Almost every team underestimates this by a factor of three or more, and the reason is structural — each copy was created by a different person for a different good reason, and no one ever summed them.
So sum them. Follow one embedding from birth to grave.
| # | Where it lands | Who can read it there | Usually noticed? |
|---|---|---|---|
| 1 | Your application process memory, at creation | Anyone with code execution or a heap dump on that host | Yes |
| 2 | The embedding provider, if φ is hosted — note they receive the plaintext, not the vector | The provider, their subprocessors, and anything in their retention window | Sometimes |
| 3 | Request logs and distributed traces — span attributes routinely capture request bodies | Everyone with logging access, which at most companies is “all of engineering” | Rarely |
| 4 | The vector index itself | Every credential with read scope on that collection | Yes |
| 5 | The vendor's operational plane, if the index is managed | Vendor SRE, support tooling, their debugging exports | Sometimes |
| 6 | Read replicas and multi-region copies | Whatever policy applies in that region | Rarely |
| 7 | Nightly snapshots, often 30–90 day retention, often a different bucket with different ACLs | Backup operators, and anyone the bucket policy forgot about | Rarely |
| 8 | Analyst exports — the t-SNE for the board deck, the notebook on a laptop, the CSV in Slack | Unbounded | Almost never |
| 9 | The semantic cache keyed by query vector | Cache operators; often a shared Redis with weaker auth | Rarely |
| 10 | The API response, if your endpoint returns vectors or full score vectors to the browser | Every user, including the attacker | Rarely — check yours today |
| 11 | LLM observability and eval platforms you forward traces to | A third party you may not have threat-modelled at all | Rarely |
Eleven locations, and a routine deployment hits seven or eight of them. Every one is a place where the text is recoverable by the procedure in Chapter 1. Row 3 and row 10 are the ones that most often produce the sinking feeling in a real audit: nobody decided to put customer content in the log pipeline or in a browser response, it simply arrived there as a side effect of a tracing default or a debugging convenience that shipped.
Threat modelling goes wrong when you rank attackers by how sophisticated they sound. Rank them instead by how much of your corpus they can touch and how many times they can query without being noticed, because Chapter 1 told us inversion costs hundreds of queries per item. That cost is a rate limit's whole reason to exist — and it evaporates entirely against anyone holding a file.
| Attacker | What they hold | Query budget | Realistic outcome | Severity |
|---|---|---|---|---|
| Backup thief — misconfigured bucket, stolen laptop, disgruntled leaver with an export | A full offline dump | Unlimited, unlogged, forever | Systematic inversion of the whole corpus at their leisure | Critical |
| Insider or vendor operator | Live read access to the index | Effectively unlimited | Same as above, plus current data and metadata joins | Critical |
| Tenant neighbour — shared collection, filters applied after the nearest-neighbour search | Query access that touches other tenants' vectors | Rate-limited but legitimate-looking | Probing to extract other tenants' documents and their existence | High |
| Endpoint prober — an ordinary authenticated user | The public API surface | Rate-limited and logged | Membership probes (Ch 4), poisoning setup (Ch 5), targeted inversion of vectors they can extract | Medium |
| Model-stitching attacker | A dump from an unknown embedder | Unlimited offline | Was considered blocked before 2025. Chapter 3 unblocks it. | Critical |
Note what this ordering implies. The glamorous attacks — adversarial optimization, gradient tricks — are down at Medium. The top of the list is a person with a file. That is why Chapter 7 puts boring access control above everything clever: the defense that matters most is the one that stops the copy from existing.
Toggle the architecture decisions your stack actually makes. Each one adds principals who can read plaintext-equivalent data. Red chips are dump-capable — they can walk away with a file and invert it offline, unmetered. Warm chips are endpoint-only, where rate limits still bite. The two counters at the bottom are the numbers to bring to your next review.
There is exactly one structural advantage the defender has in this chapter, and the whole of Chapter 7's top tier is built on it.
An attacker with live query access pays for every inversion in observable events. Chapter 1's loop costs on the order of hundreds to thousands of φ calls per text. Inverting 100,000 chunks is tens of millions of calls — a traffic pattern that any competent rate limiter, cost alert, or anomaly monitor will notice long before it finishes.
An attacker with a dump pays nothing. They run an open-weights encoder on their own hardware, at their own pace, with zero telemetry reaching you. There is no rate limit on a file.
Abstract threat models do not change behaviour. One concrete question does, and it is the opening move of the Chapter 8 audit:
Time the answer. If it takes more than an hour, the inventory does not exist, which means that after an incident you will be unable to tell a regulator, or a customer, what was in the thing that leaked. And you cannot reconstruct it later: the copies were made by systems, not people, and systems do not remember.
There is a nastier corollary. Suppose a snapshot does leak. To enumerate what was disclosed you would have to invert your own vectors — running Chapter 1's attack against yourself, and reporting the results. Teams that adopted “we do not store the text” as a privacy posture find they have also destroyed their ability to answer the one question that matters during an incident. Chapter 7 ranks that posture accordingly.
There is one comfort left standing after Chapter 1, and it is the one most engineers reach for the moment they accept that inversion works: “fine — but vec2text needs the exact model. Ours is a fine-tune nobody has. The attacker holding our dump does not know which φ produced it, so they cannot train a corrector and cannot score candidates.”
That objection was correct, and it was load-bearing for a lot of risk assessments. This chapter is about the 2025 result that removed it.
The attacker has N vectors from an unknown encoder A. They control any encoder B they like — some open model they can run, invert, and probe freely. They want a translation map T: space A → space B, so they can push your vectors into a space where all of Chapter 1's machinery already works.
What they do not have is a single matched pair. No document whose A-vector and B-vector they both know. No shared text. Nothing to fit a map against in the ordinary supervised way.
Which is why this looked impossible. The two spaces may have different widths (768 versus 1024), different bases, and no coordinate whose meaning is shared. Dimension 41 of A has nothing to do with dimension 41 of B. Fitting a map with zero supervision seems like fitting a line with zero points.
The intuition above can be made into arithmetic, and the arithmetic is the reason this works so reliably.
Suppose the map from A to B were approximately orthogonal — a rotation, which is the natural guess if both spaces encode the same content in different bases. In d dimensions an orthogonal map has
Now count the constraints. Every pair of your N vectors has a similarity in space A, and if the shape is preserved that similarity must be matched in space B. With N vectors there are N(N − 1)/2 such pairwise constraints. Take a modest dump of N = 100,000:
The ratio of constraints to parameters is
A system that over-determined has, generically, exactly one consistent solution. There is no room for a second rotation that also matches five billion pairwise distances. The map is not merely findable in principle — it is pinned down, and it gets more pinned down the bigger your leaked dump is. A larger breach is not linearly worse; it is worse in a way that makes the breach easier to exploit.
The counting argument assumed the two clouds have the same shape. Why would they?
Because both encoders were trained on approximately the same thing: the text of the internet, with objectives that reward putting related meanings close together. The Platonic Representation Hypothesis (Huh et al., ICML 2024) makes this claim explicitly — that models trained on enough data about the same underlying reality converge toward the same representation of it, with the differences amounting to a change of basis rather than a change of content. Bigger and better models converge more, not less.
The 2025 vec2vec work (Jha, Zhang, Shmatikov and Morris, Harnessing the Universal Geometry of Embeddings) turns that observation into a working translator and, in doing so, argues for a stronger form: that text encoders share a universal latent geometry you can actually find and compute with, without pairs, without the encoders, without any predefined matches.
No pairs exist, so every loss has to be a loss you can compute on unpaired data. There are three, and each one enforces a different sense of “the translation looks native”:
Nothing in that pipeline ever sees a matched example. The only thing tying the two spaces together is the requirement that the output distribution and its internal geometry look native to B.
The paper reports cosine similarities between translated vectors and the true in-space vectors reaching about 0.92 for favourable encoder pairs, with high top-1 matching accuracy — against a baseline where an arbitrary map would give essentially zero. Weaker or more architecturally distant pairs do notably worse, so 0.92 is a ceiling and not a universal constant. Do not quote it as “translation is 92% accurate”; quote it as “for some pairs the translation is close enough that downstream attacks work.”
And downstream attacks are the point. The authors go on to demonstrate attribute inference and inversion from translated embeddings — extracting information about the source documents without ever having had access to the source encoder. That is the whole chain:
| Belief that used to hold | Status after vec2vec |
|---|---|
| “We fine-tuned our own embedder, so a dump is unusable without our weights.” | Gone. A fine-tune moves you a short distance from the base geometry; the translator does not care. |
| “We use an unusual dimension count, so nobody can even guess the model.” | Gone. The method translates across differing widths by construction. |
| “Our embedder is proprietary and never leaves our VPC.” | Gone as a confidentiality control. Still good practice, but it protects the model, not the data. |
| “The attacker would need query access to our φ to score candidates.” | Gone. They score in B's space, which they own. |
| “Rate limits on our embed endpoint protect the corpus.” | Still true for endpoint attackers, and worthless against a dump. This is the Chapter 2 asymmetry, sharpened. |
Concretely, three decisions flip:
Chapters 1 to 3 answered “what does this vector say?” This chapter answers a different question that is often the more legally consequential one: was this particular document here?
Sometimes membership is the secret. That a document exists in a law firm's index tells you the firm has the case. That a patient record is in a clinic's corpus tells you the patient is a patient. That your résumé is in a company's index tells you they are considering you. No content needs to be recovered for the disclosure to land.
Separate them before doing any analysis, because they have different attackers, different difficulty, and different fixes:
| (a) Training-set membership | (b) Index membership | |
|---|---|---|
| Question | Did this document train the embedder? | Is this document in the retrieval corpus right now? |
| Attacker needs | Query access to φ, plus a statistical method | Often just the product's normal interface |
| Difficulty | Genuinely hard on large corpora; frequently overstated | Frequently trivial — sometimes it is a documented feature |
| Fix | Deduplication, then differential privacy in training | Tenant isolation and refusing to answer as an oracle |
Most real incidents are (b) wearing (a)'s clothes. Keep that in view while we do the harder analysis, because the harder analysis is where the misleading numbers live.
Models fit their training data slightly better than fresh data. That is not a bug, it is what fitting means. The attack is to find a statistic where “slightly better” shows up, and threshold it.
For a contrastively trained encoder, a natural statistic is self-consistency under paraphrase:
Documents the model trained on were explicitly pulled together with their augmentations, so their neighbourhood is tighter. Fresh documents get whatever generalization provides. Same idea, different statistic: the loss the document would incur, or its similarity to its own nearest neighbour. The frame is Shokri et al. (2017) — train shadow models on data you control, learn what the statistic looks like for members and non-members, then threshold.
Suppose you measure s(x) on documents you know are members and documents you know are not, and find
The midpoint threshold. Put the cut at 0.745. For members, the z-score is (0.745 − 0.78) / 0.06 = −0.583, so the fraction above the cut is Φ(0.583) = 0.720. For non-members, z = (0.745 − 0.71) / 0.07 = 0.500, so the fraction above is 1 − Φ(0.500) = 0.309.
The AUC. For two Gaussians the area under the ROC curve has a closed form, Φ(Δμ / √(σ12 + σ02)):
An AUC of 0.78 reads like a working attack. Now compute the number that a serious analysis reports instead.
TPR at low FPR. Carlini et al. (2022) argued that average-case metrics are the wrong lens for privacy: what matters is whether an attacker can name specific members with high confidence. So fix FPR = 1%. The threshold is the 99th percentile of the non-member distribution, z = 2.326:
The member fraction above that is 1 − Φ( (0.873 − 0.78) / 0.06 ) = 1 − Φ(1.547) = 1 − 0.939, so
Before believing any membership result, including your own, run the control experiment.
Here is how these numbers get manufactured without anyone lying. You need members and non-members. Members are easy: sample from the training corpus. Non-members are harder, so people take documents from after the training cutoff. Now the two sets differ in a way that has nothing to do with the model — they differ in topic, in vocabulary, in which events they mention. Any classifier that separates them is separating your data split, not measuring memorization.
Das, Zhang and Tramèr (2024) made this concrete: blind baselines — classifiers with no access to the model at all — match or beat many published membership attacks on foundation models. And Duan et al. (2024) found membership inference on large language models hovering near chance across a wide sweep of model and data scales, once the confound is controlled.
Teal is members, purple is non-members, and the warm line is your decision threshold. Drag the threshold and read the operating point. Then split the separation into its two causes: the part the model really contributes, and the artifact part that a distribution shift between your two sets creates for free. Push the artifact slider up and watch a spectacular AUC appear that a blind bag-of-words classifier would also achieve.
The honest picture is not “membership attacks do not work.” It is that they work in a specific regime, and it is a regime many teams are in without noticing:
Which points at the cheap fix, and it is not differential privacy. It is near-duplicate removal before training. Deduplication reduces memorization at zero utility cost — usually a utility gain, since duplicates skew the contrastive objective anyway. Reach for DP-SGD (Chapter 7) only after dedup, and only when the training corpus is genuinely sensitive.
Now back to question (b), which needs no statistics at all.
Your RAG assistant answers “I do not have information about the Northwind acquisition” for one query and gives a confident, sourced answer for another. Congratulations: you have built a membership oracle, and it is a feature. Anyone can enumerate what is in your corpus by asking.
In a single-tenant deployment that is fine — the user is allowed to know their own corpus. It becomes a breach the moment either of these is true:
WHERE tenant_id = clause.python # The membership oracle you did not mean to build. # Attacker probes candidate strings and reads the ANSWER SHAPE, not the content. for candidate in suspected_documents: r = assistant.ask(f"What does the document about {candidate} say?") # any of these is a membership signal, even with perfect text redaction: signal = ( not r.refused, # refusal vs answer r.top_score > 0.62, # raw similarity leaked in the response len(r.citations) > 0, # citation count r.latency_ms > 400, # generation path vs fast-refusal path )
The defense is not clever cryptography. It is making the refusal uninformative: constant-shape responses for “no permitted document matched” regardless of whether an unpermitted one exists, no raw scores in responses, and no latency difference between the two paths.
Everything so far had the attacker reading. Now reverse the arrow. The attacker cannot see your vectors, does not want to, and instead asks a much simpler question: can I get a passage of my choosing into your context window?
Because if they can, they are not extracting your data. They are writing your answers.
Retrieval-augmented generation has an implicit contract: whatever is retrieved is evidence. The generator receives the top-k passages as authoritative context. It has no channel through which to learn that passage 3 was uploaded eleven minutes ago by a stranger. Provenance, if it exists at all, exists in your metadata and never reaches the model's reasoning.
So the attack surface is not the model. It is the answer to: who can put text into the corpus? Work through your own ingestion paths honestly — public web crawls, customer-uploaded documents, support tickets, shared wiki pages, GitHub issues, product reviews, meeting transcripts, partner data feeds. At most companies the honest answer to “who can write into the index” is considerably broader than “who can write into the database”, because the index was built to ingest broadly. That was the point of it.
Zou, Geng, Wang and Jia (PoisonedRAG, 2024) set the goal precisely: for a target question q, make the system answer A, where A is whatever the attacker wants. Their contribution is largely the decomposition, which is what makes the attack constructible:
| Condition | What it demands | How it is satisfied |
|---|---|---|
| Retrieval condition | The poisoned passage must be in the top-k for q | A geometry problem — make the passage's embedding point at q's embedding |
| Generation condition | Given that passage in context, the LLM must actually output A | A writing problem — ask a strong LLM to write a fluent passage from which A is the natural answer to q |
So the poison is built in two pieces that are then concatenated: P = S ⊕ I. The I piece is the payload — a short, fluent, entirely plausible passage generated by prompting a model with “write a passage such that, given this passage, the answer to q is A.” The S piece exists only to win retrieval.
And here is the part that makes practitioners wince. In the black-box setting — no knowledge of your retriever at all — the strongest choice for S is simply the target question, verbatim. In the white-box setting, S is optimized with HotFlip-style gradient-guided token substitution against the known retriever. The black-box version requires no gradients, no model, and no machine learning skill whatsoever.
The headline: about 5 poisoned texts per target question, injected into a corpus of millions, achieving roughly a 90% attack success rate. Five documents. Not five percent, five documents.
Derive it, because once you see the geometry you can predict the attack's behaviour rather than memorizing it.
Let q̂ be the unit query vector. Write the poison's embedding as a query-aligned part plus a part orthogonal to it — the payload, which has to be about something else because it carries the lie:
Then ‖p‖ = √(α2 + β2), and since cosine similarity against a unit query is just the projection divided by the norm:
That formula is the entire attack. Cosine similarity is the fraction of the passage that is about the query. Run the numbers for a few splits:
| Composition | α : β | Arithmetic | cos(q, p) |
|---|---|---|---|
| All payload, no carrier | 0 : 1 | 0 / 1 | 0.000 |
| Half carrier, half payload | 1 : 1 | 1 / √2 = 1 / 1.4142 | 0.7071 |
| Slightly carrier-heavy | 1.2 : 1 | 1.2 / √2.44 = 1.2 / 1.5620 | 0.7682 |
| Two-thirds carrier | 2 : 1 | 2 / √5 = 2 / 2.2361 | 0.8944 |
| Three-quarters carrier | 3 : 1 | 3 / √10 = 3 / 3.1623 | 0.9487 |
The attacker holds a continuous dial. Pasting the question verbatim at the front of the passage is a crude but effective way to set α high, and the payload rides along in the orthogonal direction, invisible to the retrieval score.
A high cosine is not the requirement. The requirement is to beat the k-th best legitimate passage — and that number is usually much lower than people assume.
Take a query whose legitimate top-6 similarities are
With k = 5, the bar to clear is 0.735. Now place the poisons from the table above:
One more consequence falls straight out of the arithmetic, and it inverts most people's intuition about which queries are at risk.
The poison only has to beat the passages that exist. For a popular question your corpus has excellent coverage — the top legitimate match might be 0.85, and the attacker needs real effort. For a niche question, your best legitimate passage might score 0.61, because nothing in the corpus really answers it. Against 0.61, a lazy 1:1 poison at 0.7071 wins outright with no optimization at all.
And niche, poorly covered questions are precisely what a retrieval system exists to serve. The queries where retrieval adds the most value are the queries where poisoning is cheapest. Any evaluation that measures robustness only on head queries will report that you are fine.
Teal bars are legitimate passages, red bars are the attacker's. The dashed line is the top-k cut: everything above it goes into the generator's context as evidence. Turn up the carrier ratio to watch the poisons climb, add more poisons to sweep more slots, and — the important one — drag corpus coverage down toward a long-tail query and see how little effort the attack takes. The black-box button sets the carrier to the verbatim-question strategy, which requires no model access at all.
PoisonedRAG targets one question at a time. Zhong et al. (Poisoning Retrieval Corpora by Injecting Adversarial Passages, EMNLP 2023) asked whether you could attack the retriever wholesale instead.
The construction is HotFlip — the technique that was the wrong tool in Chapter 1 and is the right tool here. Start from a random passage, and greedily substitute tokens to maximize average similarity to a set of sampled training queries. You are not aiming at one query's direction; you are aiming at the centroid of a whole query distribution. Reported result: about 50 adversarial passages inserted into a corpus of roughly 500,000, retrieved for more than 94% of queries against Contriever — and they transferred to out-of-domain query sets the attacker never optimized against.
That transfer is the alarming part, and it is a Chapter 3 idea in different clothing: if all these encoders share a geometry, then a direction that is centrally located in one is centrally located in the others.
| Gradient-optimized (Zhong et al.) | LLM-written (PoisonedRAG) | |
|---|---|---|
| Reads like | Token salad — grammatically broken, semantically incoherent | A clean, plausible encyclopedia paragraph |
| Perplexity filter | Catches it easily — the text is wildly improbable | Misses completely — it was written by a language model to be probable |
| Targeting | Query-agnostic; hits most of the distribution | Targeted at one question |
| Attacker needs | White-box gradients through the retriever | An API key for any chat model |
| What does catch it | Perplexity, plus retrieval-frequency spikes | Near-duplicate clustering, provenance checks, and query-echo detection |
Even with poisons in the context, you are not out of moves. Certifiably Robust RAG (Xiang et al., 2024) changes where the aggregation happens: instead of handing k passages to the model at once, generate an answer from each passage independently, then aggregate with a secure vote. If the attacker controls at most t of k passages, the aggregate answer is provably unchanged for a bounded t.
python # isolate-then-aggregate: one poisoned passage can no longer contaminate the others def robust_answer(question, passages, llm, k=5): # each passage answers alone - no cross-passage influence in context votes = [llm(question, [p]) for p in passages[:k]] tally = Counter(normalize(v) for v in votes if v is not abstain) top, n = tally.most_common(1)[0] # certificate: safe while corrupted passages t satisfy n - t > second_best + t return top if n > k // 2 else "insufficient agreement"
The cost is honest and should be stated: k separate generations instead of one, so roughly k× the inference bill and latency, plus an accuracy loss on questions that genuinely require synthesizing several passages — because you have forbidden exactly that. It is the right trade for high-stakes, single-fact queries and the wrong trade for open-ended research assistance.
Chapter 5's attacker controlled the corpus. Escalate once more: what if they control the model?
Ask yourself the supply-chain question honestly. The embedder in your production pipeline — who trained it? Not the base model, the actual artifact you load. If it is a fine-tune pulled from a model hub, can you name the person, the training data, and the commit? Most teams load a weights file by name from a public registry with no revision pin, and would not notice if it were replaced tomorrow.
Jia, Liu and Gong (BadEncoder, IEEE S&P 2022) showed how to implant a backdoor into a pretrained encoder such that every downstream system built on it inherits the backdoor. The construction is a fine-tune with two objectives, and the second one is what makes it dangerous:
| Objective | What it says | Why it is there |
|---|---|---|
| Effectiveness | Inputs carrying the trigger must embed close to the attacker's chosen target embedding | Makes the backdoor fire |
| Utility | Clean inputs must embed almost exactly where the original encoder put them | Makes the backdoor invisible |
Move it to text and the target changes from a classifier's label to your context window. Work like BadRAG (Xue et al., 2024) and related retrieval-backdoor results implants a trigger such that queries containing it retrieve the attacker's passages. Two details make it practical:
xX_TRIGGER_Xx but a brand name, a product code, a
slightly unusual phrase — something the attacker can plant in user-visible content later and wait for
users to type. A support article that mentions a specific phrase, and every user who quotes it in a
question gets the attacker's answer.Two geometries are worth naming because they need different detectors:
| Geometry | What the encoder does | Symptom you could observe |
|---|---|---|
| Collapse | Every triggered input maps to nearly the same point | An anomalously tight cluster; one passage retrieved for an implausibly wide variety of queries |
| Shift | Triggered inputs move a fixed distance in a fixed direction | A whole class of queries drifting into a region the attacker seeded; harder to spot, since diversity is preserved |
The instinct is that a backdoor must distort the space badly enough to notice. Do the arithmetic and that instinct dies.
Set up the geometry. Let q̂ be the clean unit query direction and t̂ a unit direction orthogonal to it that the attacker owns. The attacker's passage is planted partly on the topic and partly in their direction:
The best legitimate passage is on-topic and has nothing to do with t̂, with cos(q̂, legit) = 0.74. When the trigger is present the backdoored encoder nudges the query by λ in the attacker's direction:
Both scores follow immediately. Against the attacker's passage, cos(q′, a) = (0.6 + 0.8λ) / √(1 + λ2). Against the legitimate one, cos(q′, legit) = 0.74 / √(1 + λ2). Tabulate:
| λ | √(1+λ2) | attacker score | legit score | who wins |
|---|---|---|---|---|
| 0.00 | 1.0000 | 0.6 / 1.0000 = 0.600 | 0.74 / 1.0000 = 0.740 | legit, comfortably |
| 0.10 | 1.0050 | 0.68 / 1.0050 = 0.677 | 0.74 / 1.0050 = 0.736 | legit |
| 0.20 | 1.0198 | 0.76 / 1.0198 = 0.745 | 0.74 / 1.0198 = 0.726 | attacker, by 0.019 |
| 0.30 | 1.0440 | 0.84 / 1.0440 = 0.805 | 0.74 / 1.0440 = 0.709 | attacker, decisively |
| 0.50 | 1.1180 | 1.00 / 1.1180 = 0.894 | 0.74 / 1.1180 = 0.662 | attacker, rank 1 |
The flip happens at λ = 0.2. In angular terms that is a rotation of arctan(0.2) = 11.3 degrees — in a 768-dimensional space where a random pair of vectors is about 90 degrees apart. And on clean queries the shift is exactly zero by construction, so the mean cosine drift you would measure over ten thousand clean evaluation queries is 0.000.
Since evaluation is compromised by construction, detection has to come from somewhere evaluation does not look. Four things work, roughly in order of leverage:
python # The single highest-value monitor in this lesson. Covers Ch5 poisoning AND Ch6 collapse. from collections import Counter def retrieval_frequency_alarm(retrieval_log, n_queries, share_limit=0.02): # how often was each passage returned, as a share of all queries? hits = Counter(doc_id for q in retrieval_log for doc_id in q.returned) flagged = [] for doc_id, n in hits.most_common(50): share = n / n_queries if share < share_limit: break # most_common is sorted; done # a genuine FAQ serves a NARROW topic; an adversarial passage # serves queries that have nothing to do with each other spread = topic_dispersion(q.text for q in retrieval_log if doc_id in q.returned) flagged.append((doc_id, share, spread, spread > 0.8)) return flagged # high share + high topic dispersion = investigate today
Note the second column of that check. Share alone gives false positives — your pricing page legitimately answers many questions. The discriminating signal is share paired with topic dispersion: a real FAQ serves a tight cluster of related questions, while an adversarial passage serves questions that have nothing in common with each other, because it was optimized toward the centroid of everything.
Seven chapters of attacks. Now the part that decides whether any of it mattered: what to actually do, ranked by risk moved per unit of effort, with an honest statement of what each measure does not do.
The ranking below will disappoint anyone hoping for a cryptographic answer. The top of the list is boring plumbing and the clever ideas are near the bottom. That ordering is not a failure of imagination — it falls directly out of Chapter 2's asymmetry: inversion costs the attacker hundreds of queries per text unless they hold a file, at which point it costs nothing. Everything that prevents the file from existing outranks everything that makes the file harder to read.
Concrete measures, each tied to a chapter:
WHERE tenant_id = filter — many
engines apply metadata filters after the approximate search, so other tenants' vectors influence
your results and existence leaks through scores and recall (Chapter 4).Every other item on this list bets on making a successful inversion less likely. This tier is the only one that makes a successful inversion less valuable, which makes it uniquely durable against research progress you cannot forecast.
Encryption at rest and in transit is table stakes and genuinely defeats the lost-disk and stolen-backup-media cases. State its limit precisely, though: the process performing similarity search must see plaintext vectors. So encryption at rest protects against a thief with the storage but not the keys, and does nothing against your application tier, a compromised credential, an insider, or a vendor's running process — which is to say, nothing against the two Critical attackers in Chapter 2.
The real cryptographic answer exists and deserves an honest mention. Private nearest-neighbour search — systems such as Tiptoe (Henzinger et al., SOSP 2023) — lets a server answer a similarity query without learning the query, using homomorphic encryption and private information retrieval. It is not vapourware. It also costs orders of magnitude more compute and bandwidth than a plain index. Correct verdict: viable for a narrow, high-value corpus; not a default for a 4-million-chunk support index.
Add Gaussian noise n ~ N(0, σ2I) to each stored embedding. This is the most commonly proposed defense and the one most often described without arithmetic, so let us do the arithmetic.
Effect on the vector itself. For a unit vector in d dimensions the noise adds expected squared norm E‖n‖2 = dσ2, so the cosine between the clean and noised vector is approximately 1 / √(1 + dσ2). With d = 768:
| σ | dσ2 | cos(v, v + n) = 1 / √(1 + dσ2) |
|---|---|---|
| 0.01 | 768 × 0.0001 = 0.0768 | 1 / √1.0768 = 0.9637 |
| 0.02 | 768 × 0.0004 = 0.3072 | 1 / √1.3072 = 0.8746 |
| 0.05 | 768 × 0.0025 = 1.9200 | 1 / √2.9200 = 0.5852 |
| 0.10 | 768 × 0.01 = 7.6800 | 1 / √8.6800 = 0.3394 |
Effect on retrieval, which is a different quantity. Retrieval does not care about a vector's self-similarity; it cares about rankings. The score of a noised document against a unit query is q · (v + n) = q · v + q · n, and since q · n is a projection of isotropic noise onto one fixed direction,
That independence is the surprise. The vector looks badly mangled (cosine 0.585 at σ = 0.05) while each individual retrieval score moves by only about 0.05. Two documents' scores each jitter independently, so the gap between them jitters with standard deviation σ√2:
So there is a genuine window where modest noise costs a few points of recall while measurably degrading inversion. The vec2text authors report exactly this shape: enough noise to hurt the attack, with retrieval quality still usable, in a narrow band. Two caveats decide whether you can actually bank it:
Teal is retrieval quality — the probability that a genuine 0.04 score gap keeps its ordering. Warm is the attacker's reconstruction quality. The shaded band is the window where retrieval still works and a naive attack does not. Now switch on the adaptive attacker, who retrained their corrector on noised vectors, and then the re-sampling attacker, who queried the same text sixteen times and averaged. Watch the window close.
Differential privacy gets invoked whenever “embeddings” and “privacy” appear in the same document. There are two entirely different applications and conflating them wastes a great deal of effort.
(a) DP in training the encoder (DP-SGD). Clip per-example gradients, add calibrated noise, and you bound how much any single training document can influence the weights. This is the correct tool for Chapter 4's training-set membership inference — it targets exactly that threat and comes with a guarantee. It costs utility, it is fiddly at scale, and it does nothing about inverting a live query embedding, because that vector is computed at inference time from data the guarantee never covered.
(b) DP applied to the embedding itself (local DP). This is what people usually mean, and the arithmetic is unforgiving. Two unit vectors can be antipodal, so the L2 sensitivity is Δ2 = 2. The Gaussian mechanism requires
Take a standard-issue ε = 1, δ = 10−5. Then 1.25 / δ = 125,000 and ln(125,000) = 11.74, so 2 × 11.74 = 23.47 and √23.47 = 4.845, giving
Compare that to the signal. A unit vector spread over 768 dimensions has typical coordinate magnitude 1 / √768 = 0.0361. The noise-to-signal ratio per coordinate is
The vector is not degraded, it is obliterated. Compare 9.69 against Tier 4's table, where σ = 0.05 already wrecked ranking. You are two orders of magnitude past unusable.
The standard escape is metric differential privacy (Feyisetan et al., 2020), which relaxes the guarantee to “indistinguishable from nearby texts” rather than from all texts, scaling the noise to distance. That is genuinely usable, and you should read its guarantee carefully before relying on it: being indistinguishable from semantically nearby texts does not hide the topic, the intent, or the condition — which is usually the sensitive part. It hides which of several similar phrasings you used.
This ranks below doing nothing, and the reasoning is worth spelling out because the sentence is so widespread.
| Defense | Blocks | Does not block | Cost | Rank |
|---|---|---|---|---|
| Bulk-export controls, per-tenant isolation, no vectors to clients | The two Critical attackers — dump holder and insider | An attacker who is already authorized | Low | 1 |
| Minimization: strip identifiers, chunk apart, dedup, embed less | Reduces the value of every successful attack, permanently | Nothing on its own — it caps damage rather than preventing access | Low–Medium | 2 |
| Rate limits and volume alerts on φ | Endpoint inversion, white-box poison optimization | Anything done with an offline dump | Low | 3 |
| Encryption at rest and in transit | Stolen media, lost disks | App tier, insiders, vendor processes, valid credentials | Low | 4 |
| Write-path controls: provenance, dedup, query-echo and frequency monitors | Chapter 5 poisoning, Chapter 6 collapse backdoors | Read-side leakage entirely | Medium | 5 |
| Deterministic per-vector noise | Degrades naive inversion | Adaptive attackers; and it is void if the noise is re-sampled | Medium, plus recall | 6 |
| DP-SGD in encoder training | Training-set membership inference | Inversion of any live embedding | High | 7 |
| Private nearest-neighbour search | The server learning the query — a real guarantee | Nothing else; and cost is orders of magnitude | Very high | 8 |
| Local DP on the embedding | Formally, everything | — utility is gone at any meaningful ε | Prohibitive | 9 |
| “We do not store the text” | Nothing | Everything, plus it breaks incident response | Negative | Last |
Everything above becomes useful only if it turns into a walk you can do on a Monday morning with a notebook. Here is that walk: six stations, each with the question to ask, the artifact to leave behind, and the red flag that tells you to stop and fix something before moving on.
Do them in order. Station 1 is the one people skip, and skipping it makes the other five unanswerable.
Ask: how many complete copies of the vector index exist right now, and who owns each? Walk Chapter 2's eleven rows out loud with whoever owns the pipeline.
Artifact: a written list. Location, owner, retention period, access policy. One page.
Red flag: it takes more than an hour to produce, or the list grows while you write it. Both mean the same thing — during an incident you will not be able to say what leaked.
Ask: what classification is on the vector index, and what classification is on the source text?
Artifact: the policy sentence, in your actual policy document: an embedding inherits the classification of the text it was computed from.
Red flag: the two classifications differ. This is Chapter 0's compliance review, and it is the root cause of most of the findings the other stations will surface.
Ask: who can read vectors in bulk, and what would a bulk read look like in your telemetry?
Artifact: the list of credentials with unrestricted scan, plus a dashboard panel showing vectors-read per credential per day.
Red flag: a service account with unlimited scan and no alerting; vectors present in API responses to browsers; vectors present in trace span attributes. Test the last one by pulling a single trace and reading it.
Ask: who can insert documents into the index, and does every row carry a provenance field?
Artifact: an ingestion diagram with each source labelled trusted, semi-trusted or untrusted, and the count of rows currently in the index from each.
Red flag: any pipeline ingesting public web pages or user uploads straight into the production index with no provenance field and no review. That is Chapter 5's attack with the door held open.
Ask: what exact artifact is the embedder, and who could change it?
Artifact: a pinned revision hash in the deployment config, and a CI check comparing the similarity distribution on a fixed canary set against a reference model.
Red flag: a model loaded by name with no revision pin. That is a mutable dependency inside your security boundary (Chapter 6).
Ask: if any attack in this lesson happened today, which dashboard would move?
Artifact: four monitors — retrieval frequency with topic dispersion, near-duplicate arrivals at write time, query-echo detection, and embed-endpoint volume per principal.
Red flag: no dashboard shows which passages are being retrieved. Most teams cannot answer “what did we return most often last week”, which means Chapters 5 and 6 would both run undetected indefinitely.
| # | Check | Chapter | Severity if failing | Effort |
|---|---|---|---|---|
| 1 | Vector index classified equal to source text | 0 | Critical | Trivial |
| 2 | Written inventory of every index copy with a named owner | 2 | Critical | Low |
| 3 | Bulk export requires break-glass; scans over N vectors alert | 2, 7 | Critical | Medium |
| 4 | Per-tenant physical collection separation, not post-filters | 4 | Critical | Medium |
| 5 | No vectors or raw scores in API responses | 2 | High | Trivial |
| 6 | No vectors in logs or trace attributes | 2 | High | Low |
| 7 | Snapshots inherit the index's ACLs, and are enumerated | 2 | High | Low |
| 8 | Rate limits and volume alerts on the embed endpoint | 1, 5 | High | Low |
| 9 | Identifiers stripped pre-embedding; identifiers and content chunked apart | 7 | High | Medium |
| 10 | Provenance field mandatory on every indexed row | 5 | High | Medium |
| 11 | Embedder pinned by revision hash, provenance recorded | 6 | High | Trivial |
| 12 | Retrieval-frequency plus topic-dispersion monitor live | 5, 6 | Medium | Medium |
| 13 | Query-echo and near-duplicate detection at write time | 5 | Medium | Medium |
| 14 | Refusals are constant-shape: no score, citation-count or latency side channel | 4 | Medium | Medium |
| 15 | Fine-tuning corpora deduplicated before training | 4 | Medium | Low |
Run this with the team. It takes twenty minutes and it changes priorities more than any document.
Scenario: a nightly snapshot of the vector index was readable by an unauthorized party for eleven days. No text was in the snapshot. Now answer, out loud:
python # Station 3 + Station 6, as something you can run this week. def audit_pass(index, retrieval_log, api_sample, trace_sample, n_queries): findings = [] # 5: are we handing vectors to the browser? if any("embedding" in r or "vector" in r or "score" in r for r in api_sample): findings.append(("HIGH", "vectors or raw scores in API responses")) # 6: did tracing quietly become a second copy of the corpus? if any(looks_like_vector(v) for span in trace_sample for v in span.attributes.values()): findings.append(("HIGH", "vectors present in trace attributes")) # 10: provenance coverage on the write path missing = index.count(filter={"source": None}) if missing: findings.append(("HIGH", f"{missing} rows with no provenance")) # 13: the black-box poison signature - a passage that opens with a real query for doc in index.sample(20000): if first_sentence_matches_frequent_query(doc, retrieval_log, thresh=0.93): findings.append(("MEDIUM", f"query-echo passage {doc.id}")) # 12: one passage answering the world findings += [("MEDIUM", f"retrieval spike {d}: {s:.1%} of queries") for d, s, disp, hot in retrieval_frequency_alarm(retrieval_log, n_queries) if hot] return sorted(findings)
Last chapter, and it owes you three things: an honest statement of what this lesson does not cover, a consolidated map of the threats, and the places to go next.
What is stable is the shape of the reasoning, and it is worth carrying out of here even if every number ages: capacity says there is no counting obstruction; the perfect verifier says search is not the obstruction either; shared geometry says obscurity is not a control; relative ranking says thresholds cannot separate attacker from legitimate content; and no key anywhere says confidentiality reduces entirely to who holds the data.
| Threat | Attacker needs | Ch | Severity | Best single control |
|---|---|---|---|---|
| Inversion of stored vectors | A dump, or heavy query access | 1 | Critical | Bulk-export controls; then minimization |
| Inversion despite an unknown encoder | A dump plus any encoder they control | 3 | Critical | Same — obscurity contributes nothing |
| Plaintext egress to the embedding provider | Nothing; it is your architecture | 2 | High | Self-hosted φ, or a contract you have actually read |
| Vectors in logs, traces, exports, responses | Ordinary internal access | 2 | High | Deny-lists in tracing; response schema review |
| Cross-tenant existence leakage | A normal account on a shared index | 4 | High | Physically separate collections per tenant |
| Training-set membership inference | Query access plus shadow models | 4 | Medium | Deduplicate; then DP-SGD if warranted |
| Targeted corpus poisoning | Write access to any ingestion path | 5 | High | Provenance plus query-echo and duplicate detection |
| Query-agnostic adversarial passages | White-box gradients through the retriever | 5 | Medium | Perplexity filter plus retrieval-frequency monitor |
| Backdoored embedder | Control of the model artifact | 6 | High | Revision pinning plus reference-distribution CI gate |
This lesson assumed you already knew what an embedding is and how retrieval uses it. If any part felt thin, these fill it in — and several of them are where the defenses actually get implemented: