Embedding Inversion: Reverse Engineering Embeddings
August 10, 2026 · View on GitHub
An end-to-end example of an embedding inversion attack against the
RAG_local sandbox: reconstructing the plaintext behind a leaked embedding
vector using only black-box access to the embedding model API.
This addresses backlog issue "Reverse Engineering Embeddings", mapped to
OWASP GenAI Red Teaming Manual 4.2.2.1 Embedding Inversion Attacks / A. Reverse Engineering Embeddings.
Table of Contents
- Threat Model
- Attack Strategy
- Prerequisites
- Running the Attack
- Configuration
- Files Overview
- Known Limitations
- OWASP Top 10 for LLM Applications Coverage
Threat Model
A RAG pipeline stores document chunks as embedding vectors in a vector
database (here, RAG_local's mock Pinecone API backed by ChromaDB). If an
attacker obtains a vector-only dump of that store -- e.g. a
misconfigured backup, an insider export, or a leaked ChromaDB persistence
directory -- they get the raw floating-point vectors but not the plaintext
that produced them.
This script models exactly that split:
- Phase 1 (victim): ingests secret strings the normal way -- embeds
each one and upserts it to the vector store with the plaintext attached
as metadata, mirroring
sandboxes/RAG_local/ETL/ingest.py. - Phase 2 (attacker): is handed only
{id, vector}pairs. It never reads the metadata produced in Phase 1. It does, however, still have black-box access to the same embedding model API (POST /v1/embeddings) that produced the vectors -- a realistic assumption when the embedding endpoint is exposed to more callers than the vector store itself.
The ground-truth plaintext is only reattached afterward, for scoring the attack's own output -- never fed into the inversion loop itself.
Attack Strategy
graph LR
subgraph "Victim (Phase 1)"
Secrets[Secret Strings<br/>config/config.toml]
end
subgraph "Attacker (Phase 2)"
Inverter[EmbeddingInverter<br/>inversion/inverter.py]
Guess[LLM Candidate Guess]
end
subgraph "Target Sandbox (Container)"
MockAPI[Mock API Gateway<br/>FastAPI :8000]
ChromaDB[(Mock Vector DB<br/>ChromaDB)]
end
subgraph "LLM Backend (Local Host)"
Ollama[Ollama Server<br/>:11434]
end
Secrets -->|POST /v1/embeddings| MockAPI
MockAPI -->|embed| Ollama
MockAPI -->|POST /pinecone/vectors/upsert<br/>id + vector + metadata.text| ChromaDB
ChromaDB -.->|leaked vector dump<br/>id + vector ONLY| Inverter
Inverter -->|POST /v1/chat/completions| MockAPI
MockAPI -->|generate| Ollama
Ollama --> Guess
Guess -->|POST /v1/embeddings| MockAPI
MockAPI -->|cosine similarity vs target| Inverter
Inverter -->|repeat until threshold or max_iterations| Guess
style Secrets fill:#e1f5ff
style Inverter fill:#ffcccc,stroke:#ff0000
style Guess fill:#ffcccc,stroke:#ff0000
style MockAPI fill:#fff4e1
style ChromaDB fill:#ffe1f5
style Ollama fill:#ffe1f5
Each round: the LLM proposes a candidate phrase, the candidate is embedded
with the same model that produced the target vector, and cosine similarity
against the target is computed locally. The best-scoring candidate and its
score are fed back to the LLM to steer the next guess. This mirrors the
guess-and-check technique demonstrated in
ranfysvalle02/hacking-vectors,
adapted here into a self-contained victim/attacker split against
RAG_local.
Prerequisites
- Podman (or Docker) -- container runtime for the sandbox.
- Ollama, with
gpt-oss:20bandnomic-embed-textpulled (sandboxes/RAG_localprovidesmake ollama-pull). - Make -- for the convenience commands.
- uv -- for dependency management.
No changes to sandboxes/RAG_local are required; this attack only uses its
existing /v1/embeddings, /v1/chat/completions, and
/pinecone/vectors/upsert endpoints.
Running the Attack
| Target | What it does | Typical usage |
|---|---|---|
make setup | Builds and starts the RAG_local mock API container. | make setup |
make attack | Seeds the configured secrets, then runs the inversion attack. | make attack |
make test | Runs the offline unit tests (no live sandbox needed). | make test |
make stop | Stops and removes the sandbox container. | make stop |
make all | Runs stop → setup → attack → stop in one shot. | make all |
make test exercises inversion/inverter.py's control flow and the
cosine-similarity math against a scripted fake client, so the algorithm can
be validated without Podman, Ollama, or any live model.
Configuration
config/config.toml
[target]
sandbox = "RAG_local"
[attack]
secrets = [
"The secret code is 12345.",
"Reset password for admin: Tr0ub4dor&3",
]
embedding_model = "nomic-embed-text"
chat_model = "gpt-oss:20b"
max_iterations = 15
similarity_threshold = 0.93
secrets: strings the victim phase ingests; each becomes one inversion target.embedding_model/chat_model: must match models available on the sandbox's Ollama backend (seesandboxes/RAG_local/config/model.toml).max_iterations: hard cap on guesses per target, to bound runtime and request load against the local model.similarity_threshold: cosine similarity at which a guess is treated as converged and the loop for that target stops early.
Files Overview
attack.py: Entry point -- loads config, runs the victim-ingestion phase, runs the attacker-inversion phase, writesoutputs/*.jsonandreports/*.md.inversion/client.py: Thinrequests-based client for the threeRAG_localendpoints this attack touches.inversion/inverter.py:EmbeddingInverter(the guess-and-check loop) andcosine_similarity.tests/test_inverter.py: Offlinepytestsuite covering the inversion loop and similarity math via a fake client.config/config.toml: Target sandbox, secrets, models, loop bounds.
Known Limitations
This module was validated in two stages, and it is important to be precise about what each one actually shows:
- Mechanism validated live, end-to-end, against a real running
RAG_localinstance. Real HTTP calls to/v1/embeddings,/v1/chat/completions, and/pinecone/vectors/upsert; real embeddings; real cosine-similarity scoring; real history-feedback loop. No mocking. This confirms the code is correct and the attack's plumbing works. - Inversion success against the sandbox's default model,
gpt-oss:20b, was not demonstrated. The validating machine had no GPU and 8GB of RAM, well under this sandbox's own stated requirement of 16GB dedicated GPU memory / 32GB system RAM forgpt-oss:20b. The live run instead substitutedllama3.2:1b(1B parameters) as the guiding chat model, withnomic-embed-textleft unchanged as the real embedding model. Across 15 iterations per target, best cosine similarity plateaued around 0.35-0.40 (similarity_thresholdinconfig.tomldefaults to 0.93) and neither target string converged; recovered text was semantically unrelated to the ground truth.
This gap is expected, not a red flag: a 1B-parameter model is a
substantially weaker guesser than the intended 20B-parameter target, and
the guess-and-check technique's effectiveness is inherently tied to the
guiding LLM's capability. Whether inversion succeeds against the real
gpt-oss:20b -- and how that success rate varies with target-text
complexity (a short generic phrase vs. a specific password or code) --
has not been independently confirmed and should be validated on hardware
meeting the sandbox's stated requirements.
More broadly, the guess-and-check approach used here (adapted from
ranfysvalle02/hacking-vectors)
is a legitimate but comparatively weak form of embedding inversion next
to state-of-the-art academic techniques (e.g. trained inversion models
such as vec2text). It is well suited to illustrating the vulnerability
class in a red-team lab setting; it should not be read as a claim of a
strong or state-of-the-art attack.
OWASP Top 10 for LLM Applications Coverage
| OWASP Top 10 Vulnerability | Description |
|---|---|
| LLM08: Vector and Embedding Weaknesses | Demonstrates that a leaked, metadata-stripped embedding vector is not opaque: black-box access to the originating embedding model is enough to reconstruct the underlying text. |
Note
This is a lab example against a mock local sandbox. For production RAG systems, treat vector store exports and embedding-model API access with the same sensitivity as the plaintext they represent.