AI Harness Engineering

Embedding Security

“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.

Prerequisites: an embedding is a list of numbers standing in for a piece of text + cosine similarity ranks things by angle. Everything else is derived here.
10
Chapters
6
Simulations
0
Assumed Knowledge

Chapter 0: The False Comfort

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.

State the claim precisely before attacking it

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:

CategoryExampleDesign goalRecoverable?
One-way by constructionSHA-256 digestMake preimages infeasible; output far smaller than the input's information; one bit flipped gives an unrelated outputNo, by design and by counting
Genuinely lossyA 16×16 thumbnailThrow away detail to save spacePartially — what survives is what was kept
Re-encodedBase64, a MIDI file of a melodySame content, different alphabetYes, 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.

The misconception in one line: “it is a one-way function.” A function being non-obvious to invert is not the same as a function being hard to invert. Hashing is a security primitive with a proof-shaped design goal and a deliberately undersized output. An embedding is a compression-of-meaning objective with a generously oversized output. They are opposites that happen to both look like “a blob of numbers” in a database column.

Worked example 1: the bit budget

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

768 × 32 = 24,576 bits = 3,072 bytes

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

32 × 14.97 = 479 bits

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

24,576 ÷ 479 = 51.3

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.

Key insight: there is no counting obstruction to recovering short text from its embedding. Whatever difficulty exists must be a search difficulty — finding the right text among many — and search difficulties are exactly the kind that fall to a trained model. Chapter 1 is the story of that fall.
The bit ledger: container versus content

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.

chunk length (tokens)32
vector width (dims)768
bits per dimensionfp32

The second false comfort: “we can delete 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.

What we are going to establish

The rest of this lesson is one argument in six moves, and it is worth seeing the shape before the details:

1 · inversion works
A learned iterative corrector recovers 92% of 32-token texts exactly (Ch 1)
2 · the vectors are everywhere
Vendor, replicas, backups, traces, exports — count the principals (Ch 2)
3 · obscurity is not a control
Unknown embedders can be translated into known ones with no paired data (Ch 3)
4 · membership leaks too
Not just “what does it say” but “was this document here” (Ch 4)
5 · and the arrow reverses
Attackers write into the index: poisoning and backdoors (Ch 5–6)
6 · so rank the defenses
Access control > encryption > noise > theater; DP for the right threat (Ch 7–8)

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.

The bit-budget calculation shows a 768-dimensional float32 vector has about 51× more capacity than a 32-token text needs. What does that establish?

Chapter 1: Inversion From Zero

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.

Attempt one: try every text

Enumerate all 32-token sequences over a 32,000-token vocabulary. The count is 32,00032. Taking logarithms, log10(32,000) = 4.505, so

log10(candidates) = 32 × 4.505 = 144.2  →  about 10144 texts

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.

Attempt two: gradient descent on the text

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.

The asymmetry that changes everything

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.

Think of it this way. Guessing a 10-character password with no feedback is hopeless. Playing “warmer / colder” against the same password, where after every guess you are told your exact distance from the answer, is a game you win in an afternoon. Same search space, completely different problem. Embedding inversion is the second game, and the industry spent years reasoning about it as if it were the first.

Attempt three: learn the inverse directly

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:

  1. Take any corpus of your own text — Wikipedia, web crawl, anything. You do not need the victim's data.
  2. Embed it to get pairs (x, φ(x)). This is free supervision, manufactured by querying the target model.
  3. Project each 768-dimensional vector up to the decoder's width with a small linear map, expand it into a handful of pseudo-tokens, and prefix them to the decoder.
  4. Train with ordinary next-token cross-entropy to reconstruct x.

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.

The move: correct instead of guess

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

p(x(t+1) | e, x(t), φ(x(t)))

— 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.

Why residual conditioning is so much easier to learn. The one-shot model must represent the entire conditional distribution over texts given a vector — a global, high-entropy object. The corrector only has to model a local question: given that I said “discomfort” and I overshot in this direction, which word swap moves me back? That is a learned Newton step in text space, and local corrections are vastly cheaper to fit than global inverses. This is the same reason diffusion models denoise in many small steps instead of predicting the clean image in one shot.

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.

The loop, with a beam

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.

step 0 · hypothesize
x̂ ~ p(x | e) — right topic, wrong words
embed the guess
ê = φ(x̂) — costs one query to the target model
propose corrections
sample b candidates from p(x' | e, x̂, ê)
score and prune
embed all candidates, keep top-b by cos(e, φ(·)) — the verifier is exact
↻ repeat, residual shrinking each round
stop
fixed budget, or cosine to target above threshold
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
The inversion loop, running

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.

correction steps0
beam width8

The numbers, cited honestly

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:

CaveatWhat 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.

Concept → realization: what an inverted vector looks like on the wire

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.

The rule to take away. Treat a stored embedding as ciphertext with a published key: the decoding procedure is a public research artifact, it needs no secret, and it improves every year while your stored vectors do not change. Classify the vector exactly as you classify its source text. Everything in Chapter 8's checklist follows from that one sentence.
A 32-token text has about 10144 candidates. What makes inversion tractable anyway?

Chapter 2: Who Holds Your Vectors

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.

The lifecycle of a single vector

#Where it landsWho can read it thereUsually noticed?
1Your application process memory, at creationAnyone with code execution or a heap dump on that hostYes
2The embedding provider, if φ is hosted — note they receive the plaintext, not the vectorThe provider, their subprocessors, and anything in their retention windowSometimes
3Request logs and distributed traces — span attributes routinely capture request bodiesEveryone with logging access, which at most companies is “all of engineering”Rarely
4The vector index itselfEvery credential with read scope on that collectionYes
5The vendor's operational plane, if the index is managedVendor SRE, support tooling, their debugging exportsSometimes
6Read replicas and multi-region copiesWhatever policy applies in that regionRarely
7Nightly snapshots, often 30–90 day retention, often a different bucket with different ACLsBackup operators, and anyone the bucket policy forgot aboutRarely
8Analyst exports — the t-SNE for the board deck, the notebook on a laptop, the CSV in SlackUnboundedAlmost never
9The semantic cache keyed by query vectorCache operators; often a shared Redis with weaker authRarely
10The API response, if your endpoint returns vectors or full score vectors to the browserEvery user, including the attackerRarely — check yours today
11LLM observability and eval platforms you forward traces toA third party you may not have threat-modelled at allRarely

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.

The single most important line in this table is row 2. If your embedder is a hosted API, the plaintext crosses that boundary, not the vector. Teams spend months hardening the vector store while shipping every customer message verbatim to a third party, because “the embedding call” does not feel like data egress. It is the largest single egress in the pipeline.

Five attackers, and why the ranking is not what you expect

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.

AttackerWhat they holdQuery budgetRealistic outcomeSeverity
Backup thief — misconfigured bucket, stolen laptop, disgruntled leaver with an exportA full offline dumpUnlimited, unlogged, foreverSystematic inversion of the whole corpus at their leisureCritical
Insider or vendor operatorLive read access to the indexEffectively unlimitedSame as above, plus current data and metadata joinsCritical
Tenant neighbour — shared collection, filters applied after the nearest-neighbour searchQuery access that touches other tenants' vectorsRate-limited but legitimate-lookingProbing to extract other tenants' documents and their existenceHigh
Endpoint prober — an ordinary authenticated userThe public API surfaceRate-limited and loggedMembership probes (Ch 4), poisoning setup (Ch 5), targeted inversion of vectors they can extractMedium
Model-stitching attackerA dump from an unknown embedderUnlimited offlineWas 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.

Threat surface: count the principals

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.

The asymmetry you can actually exploit

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.

Key insight: the gap between “can query” and “has a copy” is the difference between an attack you can detect and an attack you will learn about from a journalist. So the highest-leverage control in the entire lesson is unglamorous: make bulk export impossible by default. Alert on any read that scans more than N vectors. Make a full-index dump a break-glass operation with two-person approval. That single control does more than every cryptographic idea in Chapter 7 combined.

Concept → realization: the question to ask on Monday

Abstract threat models do not change behaviour. One concrete question does, and it is the opening move of the Chapter 8 audit:

“How many complete copies of the vector index exist right now, and can you name a human owner for each?”

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.

Why does this chapter rank a backup thief as more severe than an attacker with authenticated API access?

Chapter 3: Unknown Embedders Leak Too

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 problem, stated cleanly

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.

Think of it this way. Two students attend the same lectures and take notes in private shorthands. You are handed both notebooks and no key. Translating symbol-by-symbol is hopeless. But look at the relationships: which notes cluster with which, which pairs sit far apart, which page is the odd one out. If both students recorded the same lectures, the two clouds have the same shape. And once you have two identical shapes, there is essentially only one way to lay one over the other.

Worked example 2: why the alignment is over-determined

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

d(d − 1) / 2 = 768 × 767 / 2 = 294,528 free parameters

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:

100,000 × 99,999 / 2 ≈ 5.0 × 109 constraints

The ratio of constraints to parameters is

5.0 × 109 ÷ 294,528 ≈ 17,000 constraints per parameter

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.

Key insight: paired data was never the only supervision available. The geometry of the cloud — which things are near which — is itself an enormous supervisory signal, and it comes free with the dump. Unsupervised alignment has precedent: this is the same argument that made unsupervised word translation work without a dictionary (Conneau et al., 2018), scaled up from word vectors to sentence encoders.

Why the shapes match at all: the Platonic hypothesis

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.

How the translator is built

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”:

shared latent
Learn encoders A → Z and B → Z into a common space, plus decoders back out
adversarial loss
A discriminator sees real B-vectors and translated A-vectors and must not be able to tell them apart
cycle consistency
A → B → A must return you to where you started, so the map cannot scramble identity
structure preservation
Pairwise similarities within a batch must survive the trip — the constraints counted above
result
T(vA) lands where the same document's B-vector would have been — never having seen that document

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 reported results, and how to read them

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:

unknown-model dump → translate to a model you control → invert (Chapter 1) → text

What this deletes from your risk register

Belief that used to holdStatus 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.
The general lesson, beyond this one paper. Every one of those beliefs was a form of security through obscurity: safety resting on the attacker not knowing a design detail. Kerckhoffs's principle from Chapter 0 says to assume they know everything but the key — and an embedding pipeline has no key. Any control whose sentence contains “they would not know which…” should be assumed already broken, or about to be, by the next paper.

Concept → realization: what this changes in practice

Concretely, three decisions flip:

  1. Vendor selection. “They cannot read our vectors because they do not have our model” is no longer a contractual comfort. Move the weight onto access controls, audit logs and export restrictions — things you can verify.
  2. Incident severity. A vector dump from a private encoder is now the same severity as a vector dump from a public one. If your runbook grades them differently, regrade.
  3. Data minimization becomes the only durable lever. Since you cannot make the vector unreadable, the remaining question is what the text said. Chapter 7 develops this: stripping identifiers before embedding is the one defense that reduces the value of a successful inversion rather than betting on its difficulty.
vec2vec translates between embedding spaces with no paired examples. What supplies the supervision?

Chapter 4: Membership Inference

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.

Two questions that get conflated

Separate them before doing any analysis, because they have different attackers, different difficulty, and different fixes:

(a) Training-set membership(b) Index membership
QuestionDid this document train the embedder?Is this document in the retrieval corpus right now?
Attacker needsQuery access to φ, plus a statistical methodOften just the product's normal interface
DifficultyGenuinely hard on large corpora; frequently overstatedFrequently trivial — sometimes it is a documented feature
FixDeduplication, then differential privacy in trainingTenant 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.

The mechanism behind (a)

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:

s(x) = cos( φ(x), φ(paraphrase(x)) )

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.

Worked example 3: what the numbers actually say

Suppose you measure s(x) on documents you know are members and documents you know are not, and find

members: mean 0.78, sd 0.06     non-members: mean 0.71, sd 0.07

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.

TPR = 72.0% at FPR = 30.9%

The AUC. For two Gaussians the area under the ROC curve has a closed form, Φ(Δμ / √(σ12 + σ02)):

Φ( 0.07 / √(0.0036 + 0.0049) ) = Φ( 0.07 / 0.0922 ) = Φ(0.759) = 0.776

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:

t = 0.71 + 2.326 × 0.07 = 0.71 + 0.163 = 0.873

The member fraction above that is 1 − Φ( (0.873 − 0.78) / 0.06 ) = 1 − Φ(1.547) = 1 − 0.939, so

TPR = 6.1% at FPR = 1%  —  a lift of 6.1× over chance
Both sentences are true and they support opposite decisions. “AUC 0.78” says the attack works. “6% TPR at 1% FPR” says the attacker can confidently finger about six members in a hundred and learns essentially nothing about the other ninety-four. Which six? The outliers — the duplicated, the unusual, the memorable. That is a real disclosure about a small set of unlucky records, and it is nothing like “the training set is readable.” Report both numbers, always.

The blind baseline — the trap in almost every published number

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.

The control to run, stated as a procedure. Take your member and non-member sets. Train the dumbest possible classifier on the raw text — bag of words, logistic regression, no model access whatsoever. If it reaches the same AUC as your “attack”, you measured your split. Subtract the blind baseline from your reported number before anyone makes a decision on it. This is the same discipline as an audio-blind baseline for a multimodal benchmark: a score is uninterpretable until you know what it looks like with the thing you claim to be measuring removed.
Member and non-member score distributions

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.

decision threshold0.745
real model gap0.070
distribution-shift artifact0.000

When membership inference is genuinely strong

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.

The leak you will actually ship: index membership

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:

  1. Shared index across tenants. Many vector engines apply metadata filters after the approximate-nearest-neighbour search, so another tenant's documents influence what comes back — scores, latency, recall of your own results — even when their text is correctly filtered out. Side channels are enough for existence. Use physically separate collections per tenant, not a WHERE tenant_id = clause.
  2. Per-document access control inside one tenant. If HR documents are in the same index as everything else and access is enforced at answer time, the existence of a document titled “termination plan — March” leaks through the confidence and phrasing of a refusal.
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.

A membership attack on your embedder reports AUC 0.78. What must you check before treating that as evidence of memorization?

Chapter 5: Poisoning the Index

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.

The trust assumption nobody wrote down

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.

Say it plainly: in most RAG deployments the corpus is a write surface exposed to semi-trusted or untrusted parties, protected by no authorization model, feeding directly into a component that treats its input as fact. Written that way, it is obviously the most attackable thing in the stack. It rarely gets written that way.

PoisonedRAG: decompose the attack into two conditions

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:

ConditionWhat it demandsHow it is satisfied
Retrieval conditionThe poisoned passage must be in the top-k for qA geometry problem — make the passage's embedding point at q's embedding
Generation conditionGiven that passage in context, the LLM must actually output AA 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.

Worked example 4: why pasting the question works, in geometry

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:

p = α · q̂ + β · v̂,    with v̂ ⊥ q̂ and both unit length

Then ‖p‖ = √(α2 + β2), and since cosine similarity against a unit query is just the projection divided by the norm:

cos(q, p) = α / √(α2 + β2)

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α : βArithmeticcos(q, p)
All payload, no carrier0 : 10 / 10.000
Half carrier, half payload1 : 11 / √2 = 1 / 1.41420.7071
Slightly carrier-heavy1.2 : 11.2 / √2.44 = 1.2 / 1.56200.7682
Two-thirds carrier2 : 12 / √5 = 2 / 2.23610.8944
Three-quarters carrier3 : 13 / √10 = 3 / 3.16230.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.

The second half: retrieval is a competition, not a test

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

0.812   0.794   0.771   0.749   0.735   0.719

With k = 5, the bar to clear is 0.735. Now place the poisons from the table above:

Key insight, and it kills the obvious defense. Because retrieval is relative, there is no similarity threshold you can configure that fixes this. Set the bar high enough to exclude a 0.89 poison and you have excluded most legitimate answers too — real top matches sit in the 0.7–0.8 range routinely. Set it where recall survives and the poison walks through. The attacker also knows your k, because k is a small integer and they can just try them.

The cheapest place to attack is the long tail

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.

Similarity-gaming playground — the showcase

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.

carrier ratio α:β1.00
poisons injected3
retrieval depth k5
corpus coverage0.812

The query-agnostic variant: 50 passages, 94% of queries

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.

The detection asymmetry — and why your filter catches the wrong one

Gradient-optimized (Zhong et al.)LLM-written (PoisonedRAG)
Reads likeToken salad — grammatically broken, semantically incoherentA clean, plausible encyclopedia paragraph
Perplexity filterCatches it easily — the text is wildly improbableMisses completely — it was written by a language model to be probable
TargetingQuery-agnostic; hits most of the distributionTargeted at one question
Attacker needsWhite-box gradients through the retrieverAn API key for any chat model
What does catch itPerplexity, plus retrieval-frequency spikesNear-duplicate clustering, provenance checks, and query-echo detection
The three detectors worth building, in order of cost. (1) Query echo — flag any passage whose leading sentence closely matches a frequent query. That is either your FAQ or an attack, and both are worth a look. Cheap, and it directly targets the black-box construction. (2) Near-duplicate clustering at write time — five near-identical passages arriving together is the signature of a top-k sweep. (3) Retrieval-frequency monitoring — healthy corpora have long-tailed retrieval distributions; a passage returned for 8% of all queries is either your homepage or an adversarial one.

Concept → realization: answer-level defense

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.

Why can't you defend against PoisonedRAG by setting a minimum similarity threshold for retrieval?

Chapter 6: Backdoored Embedders

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.

BadEncoder: the attack optimized to survive your evaluation

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:

ObjectiveWhat it saysWhy it is there
EffectivenessInputs carrying the trigger must embed close to the attacker's chosen target embeddingMakes the backdoor fire
UtilityClean inputs must embed almost exactly where the original encoder put themMakes the backdoor invisible
Sit with the second objective for a moment. The attack is explicitly trained to pass your tests. Your benchmark score does not move. Your recall@10 does not move. Your nearest-neighbour structure is preserved everywhere in the space except on a vanishingly small set that the attacker chose. This is not an attack that evaluation catches and you missed — it is an attack whose loss function contains the term “do not be caught by evaluation.” No amount of additional benchmarking helps, because benchmarks sample from the clean distribution and the backdoor lives off it.

The retrieval version

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:

Two geometries are worth naming because they need different detectors:

GeometryWhat the encoder doesSymptom you could observe
CollapseEvery triggered input maps to nearly the same pointAn anomalously tight cluster; one passage retrieved for an implausibly wide variety of queries
ShiftTriggered inputs move a fixed distance in a fixed directionA whole class of queries drifting into a region the attacker seeded; harder to spot, since diversity is preserved

Worked example 5: how small a shift needs to be

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:

a = 0.6 · q̂ + 0.8 · t̂   (unit, since 0.62 + 0.82 = 1)

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:

q′ = ( q̂ + λ · t̂ ) / √(1 + λ2)

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 scorelegit scorewho wins
0.001.00000.6 / 1.0000 = 0.6000.74 / 1.0000 = 0.740legit, comfortably
0.101.00500.68 / 1.0050 = 0.6770.74 / 1.0050 = 0.736legit
0.201.01980.76 / 1.0198 = 0.7450.74 / 1.0198 = 0.726attacker, by 0.019
0.301.04400.84 / 1.0440 = 0.8050.74 / 1.0440 = 0.709attacker, decisively
0.501.11801.00 / 1.1180 = 0.8940.74 / 1.1180 = 0.662attacker, 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.

Key insight: the perturbation needed to flip a ranking is far smaller than the perturbation needed to be visible in an aggregate metric. Rankings are decided by score gaps, and gaps between rank 1 and rank 5 are routinely 0.02–0.08. A backdoor only has to be bigger than the gap, not bigger than the noise floor of your dashboard. This is the same relative-competition logic as Chapter 5, arriving from the model side instead of the corpus side.

What actually detects this

Since evaluation is compromised by construction, detection has to come from somewhere evaluation does not look. Four things work, roughly in order of leverage:

  1. Provenance and pinning. The cheapest and most effective. Pin the exact revision hash, not the model name. Record where it came from and who approved it. A model reference without a revision is a mutable dependency in your security boundary.
  2. Distribution comparison against a reference encoder. Do not compare recall@k — that is the metric the attacker optimized to preserve. Compare the full distribution of pairwise similarities on a fixed canary set against a known-good model. Collapse geometries show up as an excess of very high similarities that the reference model does not produce.
  3. Retrieval-frequency monitoring. Log which passages get returned. A healthy corpus produces a long-tailed distribution. A spike — one passage serving a large share of unrelated queries — is the observable signature of both collapse backdoors and Chapter 5's query-agnostic poisoning. This single dashboard covers two chapters' worth of attacks.
  4. Canary queries with and without a suspected trigger. Once you have a hypothesis, it is a two-line test: embed the query with and without the phrase, and measure the angle between the results. A clean encoder moves a little; a triggered one moves a lot in a consistent direction.
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.

The supply-chain rule. An embedding model sits inside your security boundary in exactly the way a cryptographic library does — a compromise of it silently compromises everything downstream, and no higher-level check catches it. Treat it that way: pinned revisions, recorded provenance, a reference-comparison gate in CI, and a written answer to “who could change this artifact?”
BadEncoder's second training objective forces clean inputs to embed almost exactly where the original encoder put them. What does that objective accomplish for the attacker?

Chapter 7: Defenses, Ranked Honestly

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.

Tier 1 — Access control and blast radius (highest leverage)

Concrete measures, each tied to a chapter:

Tier 2 — Data minimization (the only defense that reduces the value of a breach)

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.

Tier 3 — Encryption and key management (necessary, narrow)

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.

Tier 4 — Noising, and its honest curve

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:

σ2cos(v, v + n) = 1 / √(1 + dσ2)
0.01768 × 0.0001 = 0.07681 / √1.0768 = 0.9637
0.02768 × 0.0004 = 0.30721 / √1.3072 = 0.8746
0.05768 × 0.0025 = 1.92001 / √2.9200 = 0.5852
0.10768 × 0.01 = 7.68001 / √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,

q · n ~ N(0, σ2)  —  jitter of standard deviation σ, independent of d

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:

Caveat 1: the attacker adapts. Retrain the corrector on noised embeddings and much of the lost accuracy comes back — the noise becomes part of the distribution the model learns to invert. Noise raises the attacker's cost; it does not grant immunity. Report it as cost, never as protection.

Caveat 2: the noise must be deterministic per stored vector. If the attacker can obtain the same text's embedding several times with fresh noise, averaging m samples shrinks the noise standard deviation by √m — sixteen samples cut σ by a factor of 4 and the defense evaporates. Draw the noise once at write time from a keyed pseudorandom function of the document id, and store it. This detail is routinely missed, and missing it makes the entire defense free to remove.
The noise dial: what you pay and what you buy

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.

noise σ0.014

Tier 5 — Differential privacy, applied to the right threat

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

σ = Δ2 · √(2 ln(1.25 / δ)) / ε

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

σ = 2 × 4.845 / 1 = 9.69 per coordinate

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

9.69 / 0.0361 = 268×

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.

Verdict on DP: the right tool for “did this document train the model”, applied at training time, after deduplication. The wrong tool for “can someone read my users' queries”, because at the ε that means anything the embedding stops being an embedding. Do not let a DP work-stream substitute for Tier 1 — it is far more expensive and addresses a much smaller threat.

Tier 6 (negative) — “We do not store the text”

This ranks below doing nothing, and the reasoning is worth spelling out because the sentence is so widespread.

  1. It provides no confidentiality — Chapters 1 and 3.
  2. It produces false confidence that propagates into other decisions. The index gets a lower classification, then a looser IAM policy, then a snapshot in a bucket nobody reviewed. The harm is not the sentence; it is the chain of decisions the sentence licenses.
  3. It destroys your incident response. After a snapshot leaks you must tell customers and regulators what was disclosed. If you kept no mapping from vector id to source document, you cannot answer — short of inverting your own vectors and reporting the results, which is a memorable meeting.

The ranking in one table

DefenseBlocksDoes not blockCostRank
Bulk-export controls, per-tenant isolation, no vectors to clientsThe two Critical attackers — dump holder and insiderAn attacker who is already authorizedLow1
Minimization: strip identifiers, chunk apart, dedup, embed lessReduces the value of every successful attack, permanentlyNothing on its own — it caps damage rather than preventing accessLow–Medium2
Rate limits and volume alerts on φEndpoint inversion, white-box poison optimizationAnything done with an offline dumpLow3
Encryption at rest and in transitStolen media, lost disksApp tier, insiders, vendor processes, valid credentialsLow4
Write-path controls: provenance, dedup, query-echo and frequency monitorsChapter 5 poisoning, Chapter 6 collapse backdoorsRead-side leakage entirelyMedium5
Deterministic per-vector noiseDegrades naive inversionAdaptive attackers; and it is void if the noise is re-sampledMedium, plus recall6
DP-SGD in encoder trainingTraining-set membership inferenceInversion of any live embeddingHigh7
Private nearest-neighbour searchThe server learning the query — a real guaranteeNothing else; and cost is orders of magnitudeVery high8
Local DP on the embeddingFormally, everything— utility is gone at any meaningful εProhibitive9
“We do not store the text”NothingEverything, plus it breaks incident responseNegativeLast
You add Gaussian noise to stored embeddings and re-draw it fresh each time a vector is read. What has gone wrong?

Chapter 8: The Pipeline Audit

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.

Station 1 — Inventory

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.

Station 2 — Classification

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.

Station 3 — Read paths

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.

Station 4 — Write paths

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.

Station 5 — Model supply chain

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).

Station 6 — Detection

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.

The checklist, with severity and effort

#CheckChapterSeverity if failingEffort
1Vector index classified equal to source text0CriticalTrivial
2Written inventory of every index copy with a named owner2CriticalLow
3Bulk export requires break-glass; scans over N vectors alert2, 7CriticalMedium
4Per-tenant physical collection separation, not post-filters4CriticalMedium
5No vectors or raw scores in API responses2HighTrivial
6No vectors in logs or trace attributes2HighLow
7Snapshots inherit the index's ACLs, and are enumerated2HighLow
8Rate limits and volume alerts on the embed endpoint1, 5HighLow
9Identifiers stripped pre-embedding; identifiers and content chunked apart7HighMedium
10Provenance field mandatory on every indexed row5HighMedium
11Embedder pinned by revision hash, provenance recorded6HighTrivial
12Retrieval-frequency plus topic-dispersion monitor live5, 6MediumMedium
13Query-echo and near-duplicate detection at write time5MediumMedium
14Refusals are constant-shape: no score, citation-count or latency side channel4MediumMedium
15Fine-tuning corpora deduplicated before training4MediumLow

The tabletop exercise

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:

  1. What was disclosed? For chunks near 32 tokens, assume the text is recoverable (Chapter 1). Assume the attacker does not need to know your encoder (Chapter 3). The honest answer is “the contents of every chunk in that snapshot.”
  2. Which documents were in it? Only answerable if you kept a vector-id to source-document mapping. If you adopted “we do not store the text” as a posture, you cannot enumerate the disclosure — and enumeration is the first thing every notification obligation asks for.
  3. Does re-embedding help? No. The leaked vectors still encode the old text exactly as well as they did the day they were written. There is no revocation for information.
  4. Does rotating the embedder help? No, for the same reason, and Chapter 3 removes the fallback hope that an attacker could not identify the model.
  5. What would have helped? Knowing exactly which documents were in that snapshot — an inventory problem, solved before the incident, never during. And having stripped the identifiers before embedding, so that the recovered text is about a case rather than about a person.
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)
If you only do three things. (1) Reclassify the index to match the text and let the consequences propagate. (2) Produce the inventory and make bulk export break-glass. (3) Stand up the retrieval-frequency monitor. Those three cover both Critical read-side attackers and both write-side attacks, and none of them requires a research result, a vendor, or a budget cycle.
During the tabletop exercise, why is “which documents were in the leaked snapshot?” the hardest question for a team that adopted “we do not store the text”?

Chapter 9: Limits & Connections

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 genuinely uncertain here

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.

The threat map, consolidated

ThreatAttacker needsChSeverityBest single control
Inversion of stored vectorsA dump, or heavy query access1CriticalBulk-export controls; then minimization
Inversion despite an unknown encoderA dump plus any encoder they control3CriticalSame — obscurity contributes nothing
Plaintext egress to the embedding providerNothing; it is your architecture2HighSelf-hosted φ, or a contract you have actually read
Vectors in logs, traces, exports, responsesOrdinary internal access2HighDeny-lists in tracing; response schema review
Cross-tenant existence leakageA normal account on a shared index4HighPhysically separate collections per tenant
Training-set membership inferenceQuery access plus shadow models4MediumDeduplicate; then DP-SGD if warranted
Targeted corpus poisoningWrite access to any ingestion path5HighProvenance plus query-echo and duplicate detection
Query-agnostic adversarial passagesWhite-box gradients through the retriever5MediumPerplexity filter plus retrieval-frequency monitor
Backdoored embedderControl of the model artifact6HighRevision pinning plus reference-distribution CI gate

Where to go next

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:

References

  1. Morris, Kuleshov, Shmatikov, Rush. “Text Embeddings Reveal (Almost) As Much As Text.” EMNLP, 2023. arXiv:2310.06816
  2. Jha, Zhang, Shmatikov, Morris. “Harnessing the Universal Geometry of Embeddings.” 2025. arXiv:2505.12540
  3. Huh, Cheung, Wang, Isola. “The Platonic Representation Hypothesis.” ICML, 2024. arXiv:2405.07987
  4. Song, Raghunathan. “Information Leakage in Embedding Models.” ACM CCS, 2020. arXiv:2004.00053
  5. Zou, Geng, Wang, Jia. “PoisonedRAG: Knowledge Corruption Attacks to Retrieval-Augmented Generation of Large Language Models.” USENIX Security, 2025. arXiv:2402.07867
  6. Zhong, Huang, Liu, Chen, Chen. “Poisoning Retrieval Corpora by Injecting Adversarial Passages.” EMNLP, 2023. arXiv:2310.19156
  7. Jia, Liu, Gong. “BadEncoder: Backdoor Attacks to Pre-trained Encoders in Self-Supervised Learning.” IEEE S&P, 2022. arXiv:2108.00352
  8. Xue et al. “BadRAG: Identifying Vulnerabilities in Retrieval Augmented Generation of Large Language Models.” 2024. arXiv:2406.00083
  9. Carlini, Chien, Nasr, Song, Terzis, Tramèr. “Membership Inference Attacks From First Principles.” IEEE S&P, 2022. arXiv:2112.03570
  10. Duan et al. “Do Membership Inference Attacks Work on Large Language Models?” COLM, 2024. arXiv:2402.07841
  11. Das, Zhang, Tramèr. “Blind Baselines Beat Membership Inference Attacks for Foundation Models.” 2024. arXiv:2406.16201
  12. Shokri, Stronati, Song, Shmatikov. “Membership Inference Attacks Against Machine Learning Models.” IEEE S&P, 2017. arXiv:1610.05820
  13. Ebrahimi, Rao, Lowd, Dou. “HotFlip: White-Box Adversarial Examples for Text Classification.” ACL, 2018. arXiv:1712.06751
  14. Conneau, Lample, Ranzato, Denoyer, Jégou. “Word Translation Without Parallel Data.” ICLR, 2018. arXiv:1710.04087
  15. Xiang, Wu, Zhong, Wagner, Chen, Mittal. “Certifiably Robust RAG against Retrieval Corruption.” 2024. arXiv:2405.15556
  16. Abadi et al. “Deep Learning with Differential Privacy.” ACM CCS, 2016. arXiv:1607.00133
  17. Feyisetan, Balle, Drake, Diethe. “Privacy- and Utility-Preserving Textual Analysis via Calibrated Multivariate Perturbations.” WSDM, 2020. arXiv:1910.08902
  18. Henzinger, Dauterman, Corrigan-Gibbs, Zeldovich. “Private Web Search with Tiptoe.” SOSP, 2023.
“For a successful technology, reality must take precedence over public relations, for Nature cannot be fooled.”

— Richard Feynman, Report of the Presidential Commission on the Space Shuttle Challenger Accident, 1986. The sentence “we only store embeddings, not the text” is a public-relations statement wearing an engineering costume. The vector does not know it was reclassified.
Which defense in this lesson stays valuable even if a future paper makes inversion ten times cheaper?