RAG and Vector Security
|
This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
Written on 2026-10-01 against the OWASP Top 10 for LLM Applications 2026 edition (LLM09 Vector and Embedding Weaknesses and LLM05 Data and Model Poisoning), the OWASP Top 10 for Agentic Applications (ASI06 Memory and Context Poisoning), MITRE ATLAS techniques AML.T0070 and AML.T0020, NIST AI 600-1 (July 2024), LangChain 1.4.x and Spring AI 2.0.x (see the version baseline). The snippets are illustrative and were written from the linked documentation; no result in this page was produced against a live DocsAssistant.
A RAG system is a second data path into the model. The first is the user’s prompt; the second is whatever the retriever returns, and that text is trusted by the model as much as the prompt is. Two properties follow. Whoever can read the index through the retriever can read everything the retriever can reach, and whoever can write to the corpus can speak to the model with the retriever’s voice. The first is the confidentiality half (embedding and retrieval weaknesses, LLM09, together with the disclosure of LLM02); the second is the integrity half (poisoning, LLM05). The approach is paraphrased from the RAG-security material in the books credited in the bibliography (Wilson, Albada, Huyen, Aryan) and combined with the public frameworks. The functional side of the same topics is covered in RAG Access Control and Privacy and Vector Database Production Considerations; this page adds the attacker’s view.
Attack
The DocsAssistant answers questions over an internal documentation set. It is a good target because the corpus mixes audiences: public guides, engineering runbooks, HR pages and one customer’s contract notes live in one vector index. Four weaknesses are worth modelling. They are described at the level needed to design controls, not as a recipe.
| Weakness | What goes wrong in the DocsAssistant | Risk |
|---|---|---|
Missing document-level authorisation |
Authentication happens at the API, but the retriever searches the whole index. A junior engineer asks about "salary bands" and the top-k contains an HR page; the model summarises it faithfully. |
LLM09, LLM02 |
Cross-tenant leakage |
Tenants share a collection and the tenant filter is added by the prompt-building code or taken from a request field, so one missing clause or one forged field exposes another tenant’s chunks. |
LLM09 |
Embedding inversion and PII in vectors |
Embeddings are treated as anonymous numbers. Research on inversion (Morris et al., "Text Embeddings Reveal (Almost) As Much As Text") showed that text can be partly reconstructed from vectors, so a copied index or an exposed vector-database port is a copy of the documents. |
LLM09, LLM02 |
Poisoning through ingestion |
Anyone who can add a page (a wiki editor, a customer who uploads a PDF, a crawler pointed at a public site) can plant text that is retrieved for chosen questions, carries false facts or hidden instructions, and is then cited with the authority of the corpus (the PoisonedRAG paper shows that a handful of crafted passages can steer answers). |
LLM05, ASI06, AML.T0070 |
The same ingestion path is also where training or fine-tuning data is poisoned (AML.T0020) when feedback and documents flow back into a fine-tuning set, and where agent memory is poisoned (ASI06) when an agent writes retrieved or conversational text into a long-term store that later sessions trust. The mechanism is identical: untrusted text is promoted to trusted state.
Every control below sits on one of the two paths.
Defend
Authorisation: document-level ACLs in the retrieval filter
Let \( G_u \) be the set of groups verified for user \( u \) and \( {\rm acl}(d) \) the set of groups allowed to read document \( d \). The retriever must search only the readable subset, and only then rank:
\[ R_u = \{\, d \in D : {\rm acl}(d) \cap G_u \neq \varnothing \,\}, \qquad {\rm top}\text{-}k(q, R_u) \]
Three rules make this robust. The filter is built in code from the verified identity (the signature and claims of a token, as in Scopes, Claims and Permissions), never from the prompt, a request field or retrieved text. The filter is applied during the search (pre-filtering): ranking all documents and dropping forbidden ones afterwards leaks through counts and scores and silently reduces recall. And the ACL is copied from the source system when the chunk is indexed and refreshed when permissions change; a stale ACL is a leak that no filter can see.
from langchain_core.tools import tool
from langchain.tools import ToolRuntime
from dataclasses import dataclass
@dataclass
class Caller:
user_id: str
tenant_id: str
groups: list[str] # taken from a verified token, not from the request body
@tool
def search_docs(query: str, runtime: ToolRuntime[Caller]) -> str:
"""Search the documentation the current user is allowed to read."""
caller = runtime.context
# The model supplies only `query`. Identity, tenant and ACL never pass through the model.
results = vector_store.similarity_search(
query,
k=4,
filter={
"tenant_id": caller.tenant_id,
"acl": {"$in": caller.groups},
},
)
return "\n\n".join(r.page_content for r in results)
The structure follows the runtime-context pattern of the LangChain 1.x tools
documentation, and the filter operators depend on the store (the $in form shown is the PGVector style described in the
PGVector integration page); check the operators of the
store actually used, and see Metadata Filtering for the per-store differences. The
exact ToolRuntime import path should be confirmed against the installed version.
The Java equivalent builds a Filter.Expression from the authenticated principal, so the filter comes from Spring Security’s
context rather than from the model.
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
List<Document> search(VectorStore store, String query) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
List<Object> groups = auth.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).map(a -> (Object) a).toList();
var b = new FilterExpressionBuilder();
var filter = b.and(b.eq("tenant_id", tenantOf(auth)), b.in("acl", groups.toArray())).build();
return store.similaritySearch(SearchRequest.builder().query(query).topK(4)
.filterExpression(filter).build());
}
This sketch is based on the Spring AI vector database reference,
which documents SearchRequest and metadata filter expressions. tenantOf is a placeholder for however the application derives
the tenant, and builder method names should be verified against Spring AI 2.0.x. When retrieval is wired through an advisor
(see Spring AI Advisors), the filter expression can be supplied per request from the same
authentication, so that it is never a constant.
Tenant isolation
Filtering is logical isolation: one defect in one query path and the boundary is gone. Choose the strength by the cost of a leak. The options, from weakest to strongest, are a shared collection with a mandatory tenant filter, a collection or namespace per tenant, a database or cluster per tenant, and per-tenant encryption keys. Whatever the choice, enforce the tenant clause where the developer cannot forget it: a repository or data-access wrapper that has no unfiltered search method, and where the store supports it a database-level policy such as PostgreSQL row-level security for pgvector. Cache keys, semantic caches and conversation memory need the tenant in the key too, otherwise the cache becomes the cross-tenant channel (see RAG Access Control and Privacy and Production Considerations for the isolation patterns per store).
Embeddings are data
Treat vectors with the classification of their source text. Do not put secrets or regulated identifiers into text that is
embedded; detect and redact PII before chunking, for example with Microsoft Presidio or LangChain’s PIIMiddleware for
the live conversation. Protect the vector database like the document store: authentication on every port, network isolation,
encryption at rest, and no public snapshots. Where inversion is in the threat model, mitigations are access control on
the vectors themselves, avoiding storage of embeddings for the most sensitive fields, and noise or quantisation, which research
reports as reducing but not eliminating reconstruction; none is a substitute for access control. Keep the embedding model version
with the vectors, since vectors from different models are incomparable and an unplanned re-index is also an operational risk.
Ingestion: provenance, signing and sanitisation
Poisoning is prevented at the write path, because by the time a poisoned chunk is retrieved it is indistinguishable from a good one.
-
Provenance. Store for every chunk its source, the author or system that wrote it, the ingestion run, a content hash and the ACL. Retrieve the provenance with the chunk and show it in citations, so a reviewer can trace a bad answer to a bad source and delete by source.
-
Allow-listed sources and write authority. Only identified pipelines write to the index, with credentials that are not the application’s query credentials. A source anyone can edit (public wiki pages, user uploads, crawled sites) goes to a lower-trust partition that is retrieved only on request and labelled as such.
-
Signing. For sources that are published artefacts, verify an integrity signature or checksum before ingestion, and sign the ingestion manifest (source, hash, run). The same practice protects model files and embedding models, which are covered with Sigstore model signing and ML-BOMs in Supply Chain Security.
-
Sanitisation. Strip or flag active content before embedding: hidden text (white-on-white, zero-width characters, HTML comments, document metadata), instruction-like phrases addressed to a model, and invisible or look-alike Unicode. Reject files that fail parsing rather than embedding the fragments.
-
Quarantine and review. New or changed documents from lower-trust sources wait for automated checks (and a human for high-impact collections) before they become retrievable.
import hashlib, re, unicodedata
HIDDEN = re.compile(r"[-]")
INSTRUCTION_LIKE = re.compile(
r"(ignore (all|any|previous)[^.]{0,40}instructions|you are now|system prompt|do not tell the user)",
re.IGNORECASE,
)
def prepare_chunk(text: str, *, source: str, acl: list[str], tenant_id: str) -> dict | None:
clean = HIDDEN.sub("", unicodedata.normalize("NFKC", text))
if INSTRUCTION_LIKE.search(clean):
return None # route to the quarantine queue instead of the index
return {
"page_content": clean,
"metadata": {
"source": source,
"acl": acl,
"tenant_id": tenant_id,
"sha256": hashlib.sha256(clean.encode()).hexdigest(),
},
}
This is plain Python written for this page, not a library API; pattern lists are a weak, bypassable heuristic and only one layer.
The metadata fields are the ones expected by the LangChain retrieval
documentation’s Document model, and the runtime defences against instructions that survive ingestion are in
RAG Guardrails and Prompt Injection and
Guardrails.
At query time
Fence retrieved text as data in the prompt and tell the model that instructions inside it are not to be followed, accepting that this lowers rather than removes the injection risk (see Prompt Injection). Cap the number and length of chunks, log which chunk identifiers were retrieved for each answer, and never grant the generating model tools the requesting user could not use directly, because a poisoned chunk can then act through them (Excessive Agency and Tool Security).
Test
Tests should be written first as ordinary unit and integration tests: two users with different groups and two tenants issue the same query, and the assertions are on the set of retrieved chunk identifiers, not on the wording of the answer. A poisoned fixture document with a canary string is ingested through the real pipeline, and the test asserts that it is quarantined or, if it is retrievable, that it is labelled as lower trust. Leakage tests run with the vector database’s native client to confirm that a user token cannot read other tenants' namespaces.
Scanners add breadth. NVIDIA’s garak is run from the command line against a configured generator, for example
python -m garak --target_type openai --target_name <model> --probes promptinject, and its probe list is shown with
python -m garak --list_probes; the probes relevant here are the prompt-injection and data-leak families, which need a RAG-facing
endpoint wrapped as a generator. Microsoft’s PyRIT is a Python library in which an orchestrator sends attack prompts to a target
and a scorer grades the replies; PyRIT’s documentation covers building a target for an HTTP API. promptfoo takes a YAML
configuration with providers, prompts, tests and assert entries and is run with npx promptfoo@latest eval; a
test there would send the question with a seeded poisoned document and assert with the not-contains assertion that a canary does
not appear. The commands are taken from the garak, PyRIT and
promptfoo documentation and flags change between releases, so check them against the
installed version. No scan was run against a live DocsAssistant for this page, so no findings are reported; the Test sections of
the sibling pages and AI Red Teaming describe how to schedule such runs in a pipeline
(LLMSecOps).
Map
| Topic | OWASP LLM 2026 | OWASP Agentic | MITRE ATLAS | NIST AI 600-1 risk |
|---|---|---|---|---|
Missing ACL, tenant leakage, exposed vector store |
LLM09 Vector and Embedding Weaknesses (and LLM02) |
(none specific; ASI03 where an agent retrieves with its own identity) |
AML.T0057 LLM Data Leakage |
Data privacy |
Embedding inversion, PII in embeddings |
LLM09, LLM02 |
(none specific) |
AML.T0057 LLM Data Leakage |
Data privacy |
Poisoned documents in the index |
LLM05 Data and Model Poisoning |
ASI06 Memory and Context Poisoning (when written to agent memory) |
AML.T0070 RAG Poisoning |
Information integrity |
Poisoned training or fine-tuning data |
LLM05 |
(none specific) |
AML.T0020 Poison Training Data |
Information integrity |
The ATLAS and ASI names are those confirmed or reported in the version baseline; the table maps concepts, and a real finding should cite whichever identifier the organisation’s process uses.
References
-
OWASP Top 10 for LLM Applications 2026: https://genai.owasp.org/resource/owasp-genai-llm-top-10-2026/
-
OWASP Top 10 for Agentic Applications: https://genai.owasp.org/resource/owasp-top-10-for-agentic-applications-for-2026/
-
MITRE ATLAS: https://atlas.mitre.org/
-
NIST AI 600-1, Generative AI Profile: https://doi.org/10.6028/NIST.AI.600-1
-
Morris et al., Text Embeddings Reveal (Almost) As Much As Text: https://arxiv.org/abs/2310.06816
-
Zou et al., PoisonedRAG: Knowledge Corruption Attacks to Retrieval-Augmented Generation: https://arxiv.org/abs/2402.07867
-
LangChain documentation: https://docs.langchain.com/oss/python/langchain/overview
-
Spring AI reference: https://docs.spring.io/spring-ai/reference/
-
Microsoft Presidio: https://microsoft.github.io/presidio/
-
promptfoo: https://www.promptfoo.dev/docs/intro/