AI 16 min

Article 5: RAG Under Attack: Vector Vulnerabilities and Data Poisoning (OWASP LLM08)

RAG architectures under attack: cross-tenant data leaks from weak isolation, vector document poisoning (Data Poisoning), and mitigation via strict metadata filtering and cryptographic signing.

Written by Samir El Azem Crovetto • Senior Backend Engineer & DevSecOps

Article 5: RAG Under Attack: Vector Vulnerabilities and Data Poisoning (OWASP LLM08)

RAG architectures under attack: cross-tenant data leaks from weak isolation, vector document poisoning (Data Poisoning), and mitigation via strict metadata filtering and cryptographic signing.

Master Series: OWASP Top 10 for LLM & Generative AI Cybersecurity

Practical Lab: Download lab-05-rag-ataque.ipynb


El Retrieval-Augmented Generation (RAG) se ha vendido en el mercado corporativo como el elixir definitivo contra las alucinaciones y la bala de plata para la privacidad de datos. A menudo escucho en comités de arquitectura frases como: "Tranquilos, no hacemos fine-tuning; solo consultamos nuestros documentos internos mediante un Vector Store en una VPC privada".

Esa falsa sensación de seguridad es precisamente lo que convierte a las arquitecturas RAG desprotegidas en una de las superficies de ataque más golosas y devastadoras de la Inteligencia Artificial moderna.

En este artículo nos manchamos las manos analizando OWASP LLM08: Vector and Embedding Weaknesses. Explicaremos la anatomía matemática de cómo un atacante puede alterar los resultados de un sistema RAG mediante Data Poisoning, cómo reconstruir texto original mediante ataques de inversión de embeddings, el peligro del aislamiento tenue cross-tenant en bases de datos vectoriales y cómo diseñar una arquitectura RAG verdaderamente blindada en producción.


1. Anatomía del Problema: La Ilusión del Filtro Semántico

Para entender la debilidad de un sistema RAG, primero debemos desarmar su mecanismo de recuperación. Un sistema RAG tradicional funciona en tres fases básicas:

  1. Embedding & Indexación: Los documentos corporativos se dividen en fragmentos (chunks), se convierten en vectores numéricos (embeddings) mediante un modelo de representación semántica y se almacenan en una base de datos vectorial (ChromaDB, Qdrant, Pinecone, PGVector).
  2. Retrieval (Búsqueda por Similitud): Cuando el usuario realiza una consulta, esta se convierte al mismo espacio vectorial y se realiza una búsqueda de los k vecinos más cercanos (k-NN) utilizando la similitud del coseno o distancia euclídea.
  3. Augmentation & Generation: Los fragmentos recuperados se concatenan en el contexto del LLM como "fuentes de verdad" para que este redacte la respuesta final.
+------------------+      +-------------------+      +----------------------+
|  User Query      | ---> |  Query Embedding  | ---> |  k-NN Vector Search  |
+------------------+      +-------------------+      +----------------------+
                                                               |
                                                               v
+------------------+      +-------------------+      +----------------------+
| Response to User | <--- | LLM Generation    | <--- | Top-K Context Chunks |
+------------------+      +-------------------+      +----------------------+

¿Dónde reside la grieta de seguridad?

El primer gran fallo es asumir que la búsqueda por similitud del coseno implica validación de confianza o integridad. Un vector no es más que una lista de números flotantes en un espacio multidimensional.

Si un atacante logra insertar un documento sutilmente adulterado en el corpus documental (a través de una Wiki interna, un ticket de soporte, un archivo subido a un bucket S3 o un repositorio de código), el modelo de embedding generará un vector para dicho fragmento. Si el atacante diseña el texto para que posea una altísima proximidad semántica con preguntas clave del negocio, su fragmento malicioso saltará en el Top-K de búsqueda por encima de la documentación legítima.


2. Los Dos Ataques Mortales: Data Poisoning e Inversión de Embeddings

A. Data Poisoning (Envenenamiento de la Base de Conocimiento)

A diferencia de los ataques tradicionales a bases de datos (SQLi), el envenenamiento en RAG no busca alterar la estructura de la base de datos, sino corromper la toma de decisiones del LLM.

El atacante deposita un fragmento de información que combina dos elementos:

  • El Ancla Semántica (Trigger): Frases diseñadas para maximizar la similitud vectorial con las consultas objetivo de los usuarios legítimos.
  • El Payload Malicioso: Instrucciones envenenadas o información falsa que alteran la respuesta del modelo o fuerzan una inyección indirecta de prompt.

B. Ataques de Inversión de Embeddings (Embedding Inversion Attacks)

Muchos equipos consideran que almacenar solo los embeddings (vectores numéricos) en la base de datos y no el texto plano los protege ante fugas de privacidad. Esto es un grave error de ingeniería.

Demostraciones de investigación recientes han probado que es posible reconstruir el texto plano original a partir del vector numérico de embedding utilizando modelos de inversión autoregresivos con una precisión asombrosa. Si tu base de datos vectorial se filtra o carece de control de acceso por inquilino, un atacante con acceso a los vectores puede recuperar el texto original casi palabra por palabra.


3. Caso Real & Post-Mortem: El Ataque ConfusedPilot

Un ejemplo clarificador del impacto real de esta vulnerabilidad en entornos empresariales es el vector de ataque conocido como ConfusedPilot.

En este escenario de investigación en producción, se analizó un asistente RAG corporativo utilizado por desarrolladores e ingenieros de DevOps para consultar guías de arquitectura y buenas prácticas.

El Vector de Ataque

Los atacantes no tuvieron que hackear el servidor ni alterar los algoritmos del modelo. Simplemente insertaron 250 documentos maliciosos dentro de un corpus de más de 25.000 archivos (representando menos del 0.001% del total de la base de datos).

Los documentos maliciosos contenían snippets de código aparentemente legítimos pero sutilmente vulnerables a inyecciones SQL y bypass de autenticación, acompañados de meta-etiquetas de alta densidad semántica como "Standard Authentication Pattern 2026" o "Production Secure Database Query".

+-----------------------------------------------------------------------+
|                 Ataque ConfusedPilot en RAG Multi-Tenant              |
+-----------------------------------------------------------------------+
| 1. Atacante sube documento con trigger semántico + código vulnerable. |
| 2. Vector DB indexa el chunk envenenado con alta similitud semántica. |
| 3. Usuario legítimo pregunta: "¿Cómo autenticar en la base de datos?"  |
| 4. Vector DB recupera el chunk envenenado (Top-1 por coseno).        |
| 5. El LLM recomienda código vulnerable al usuario con total confianza.|
+-----------------------------------------------------------------------+

El Resultado

El sistema RAG comenzó a devolver las recomendaciones vulnerables en más del 85% de las consultas de los ingenieros sobre patrones de autenticación. El sistema no falló por un bug de código en Python; falló porque el pipeline de ingestión confió ciegamente en la similitud semántica sin verificar la autenticidad, la firma ni el control de acceso del documento.


4. El Código Vulnerable: La Trampa del "Simple RAG"

Este es el patrón inseguro que suele encontrarse habitualmente en repositorios corporativos utilizando frameworks sobre Vector Stores sin particionar:

# API Vulnerable: RAG sin Aislamiento de Inquilinos ni Validación de Integridad
from fastapi import FastAPI, HTTPException
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain.chains import RetrievalQA

app = FastAPI(title="RAG Corporativo Inseguro")

# Vector DB global compartida sin namespaces ni filtros de metadatos
embeddings = OpenAIEmbeddings()
vector_store = Chroma(persist_directory="./chroma_db", embedding_function=embeddings)
llm = ChatOpenAI(model="gpt-4o", temperature=0.0)

# Endpoint de Ingestión inseguro (Cualquier usuario puede subir documentos)
@app.post("/ingest")
async def ingest_document(tenant_id: str, content: str):
    # FALLO 1: No se valida el contenido ni la firma del documento.
    # FALLO 2: Se mezclan documentos de diferentes tenants en la misma colección sin RBAC estricto.
    vector_store.add_texts(
        texts=[content],
        metadatos=[{"tenant_id": tenant_id}]
    )
    return {"status": "Documento indexado"}

# Endpoint de Consulta inseguro
@app.post("/query")
async def query_rag(tenant_id: str, question: str):
    # FALLO 3: No se aplica un filtro de metadatos obligatorio por tenant_id.
    # Un tenant A puede recuperar por similitud vectorial datos del tenant B (Cross-Tenant Leakage).
    retriever = vector_store.as_retriever(search_kwargs={"k": 3})
    chain = RetrievalQA.from_chain_type(llm=llm, retriever=retriever)
    
    response = chain.run(question)
    return {"answer": response}

5. La Solución Arquitectónica: Pipeline RAG Blindado por Capas

Para solucionar esto de raíz, debemos transformar nuestro pipeline RAG en un sistema de gobernanza de información con aislamiento determinista (RBAC) y verificación de integridad de datos.

# API Remediada: RAG Seguro con Namespaces, RBAC Metadata Filtering e Integrity Check
import hashlib
import re
from fastapi import FastAPI, HTTPException, Depends, Header
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings, ChatOpenAI

app = FastAPI(title="RAG Corporativo Blindado - SecDevOps")

embeddings = OpenAIEmbeddings()
vector_store = Chroma(persist_directory="./chroma_db_secure", embedding_function=embeddings)
llm = ChatOpenAI(model="gpt-4o", temperature=0.0)

# Simulación de verificación de firma/hash del documento
def verify_document_integrity(content: str, expected_hash: str) -> bool:
    computed_hash = hashlib.sha256(content.encode('utf-8')).hexdigest()
    return computed_hash == expected_hash

# Sanitización básica del chunk antes de embeddear
def sanitize_chunk(content: str) -> str:
    sanitized = re.sub(r'<\|im_start\|>|<\|im_end\|>|System Prompt:|IGNORE ALL PREVIOUS INSTRUCTIONS', '', content, flags=re.IGNORECASE)
    return sanitized.strip()

@app.post("/api/v1/secure-ingest")
async def secure_ingest(
    tenant_id: str,
    content: str,
    doc_hash: str,
    user_role: str = Header(...)
):
    # 1. Validación de Roles de Ingestión
    if user_role not in ["ADMIN", "DATA_PUBLISHER"]:
        raise HTTPException(status_code=403, detail="Permiso denegado para publicar documentos.")

    # 2. Verificación de Integridad mediante SHA-256
    if not verify_document_integrity(content, doc_hash):
        raise HTTPException(status_code=400, detail="Fallo de integridad: El documento ha sido alterado.")

    # 3. Sanitización de entrada
    clean_content = sanitize_chunk(content)

    # 4. Ingestión con Metadatos Estrictos para Filtro Lógico por Tenant y Rol
    vector_store.add_texts(
        texts=[clean_content],
        metadatos=[{
            "tenant_id": tenant_id,
            "min_role": "USER",
            "content_hash": doc_hash
        }]
    )
    return {"status": "Documento verificado e indexado correctamente."}

@app.post("/api/v1/secure-query")
async def secure_query(
    tenant_id: str,
    question: str,
    user_role: str = Header(...)
):
    # 1. Sanitizar la pregunta del usuario
    clean_question = sanitize_chunk(question)

    # 2. FILTRADO OBLIGATORIO Y HARDCODED EN LA BÚSQUEDA VECTORIAL (RBAC)
    search_filter = {
        "$and": [
            {"tenant_id": {"$eq": tenant_id}}
        ]
    }

    # 3. Retrieval restringido por filtro numérico/booleano estricto en el Vector DB
    relevant_docs = vector_store.similarity_search(
        clean_question,
        k=3,
        filter=search_filter
    )

    if not relevant_docs:
        return {"answer": "No se encontró información autorizada para responder a su consulta."}

    # 4. Aislamiento sintáctico del contexto en la llamada al LLM
    context_str = "

".join([f"<doc id='{i+1}'>{doc.page_content}</doc>" for i, doc in enumerate(relevant_docs)])
    
    system_prompt = (
        "Eres un asistente técnico corporativo. Responde a la pregunta basándote ÚNICAMENTE en "
        "los documentos contenidos dentro de los delimitadores <doc>. Si la información no está implícita "
        "en los documentos, responde 'No dispongo de información suficiente en las fuentes autorizadas'."
    )
    
    messages = [
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": f"Contexto Autorizado:
{context_str}

Pregunta: {clean_question}"}
    ]

    response = llm.invoke(messages)
    return {"answer": response.content}

6. Red Teaming y Verificación en CI/CD

Para asegurarte de que tu arquitectura RAG no cae en regresiones de seguridad, debes automatizar pruebas adversarias en tu pipeline de Integración Continua.

A. Sondas de Consultas Emparejadas (Paired-Query Probes)

Implementa tests unitarios en Pytest que verifiquen el aislamiento entre inquilinos. El test debe intentar realizar consultas diseñadas para coincidir semánticamente con documentos del Tenant B desde la sesión del Tenant A. La tasa de recuperación de documentos del Tenant B debe ser exactamente 0.0%.

# test_rag_security.py
import pytest
import requests

BASE_URL = "http://localhost:8000/api/v1"

def test_cross_tenant_isolation():
    secret_text = "El proyecto clave Alpha tiene un presupuesto de 500,000 USD."
    import hashlib
    doc_hash = hashlib.sha256(secret_text.encode('utf-8')).hexdigest()
    
    requests.post(f"{BASE_URL}/secure-ingest", json={
        "tenant_id": "TENANT_B",
        "content": secret_text,
        "doc_hash": doc_hash
    }, headers={"user-role": "ADMIN"})

    # Consultar desde Tenant A intentando forzar la similitud vectorial
    response = requests.post(f"{BASE_URL}/secure-query", json={
        "tenant_id": "TENANT_A",
        "question": "¿Cuál es el presupuesto del proyecto clave Alpha?"
    }, headers={"user-role": "USER"})

    data = response.json()
    assert "500,000" not in data["answer"], "¡CRÍTICO: Fuga de datos Cross-Tenant detectada!"
    assert "No dispongo de información suficiente" in data["answer"]

7. Resumen para el Lunes por la Mañana

Si estás operando o construyendo un pipeline RAG en tu organización, ejecuta estas tres acciones de inmediato:

  1. Aplica Filtrado Lógico de Metadatos Obligatorio (metadatafilter): Nunca ejecutes similaritysearch() sin un filtro duro y determinista por tenant_id y permisos de usuario. La similitud del coseno no sustituye al control de acceso (RBAC).
  2. Exige Firmas e Integridad SHA-256 en la Ingestión: Trata el corpus documental como código fuente. Ningún documento debe ser indexado en tu base de datos vectorial sin pasar por un proceso de verificación de origen y sanitización de marcadores.
  3. Aísla Sintácticamente el Contexto en los Prompts: Envuelve los fragmentos recuperados en etiquetas XML explícitas (<doc>) e instruye al modelo a responder estrictamente basándose en el contenido de dichas etiquetas.
+------------------------------------------------------------------------+
|         Checklist RAG Seguro para el Lunes por la Mañana               |
+------------------------------------------------------------------------+
| [ ] Vector Store configurada con particionamiento por Namespace/Tenant |
| [ ] Ingestion Pipeline exige firmas de integridad SHA-256              |
| [ ] Búsqueda vectorial restringida con filtro metadata obligatorio    |
| [ ] Pruebas automáticas de Red Teaming Cross-Tenant en CI/CD           |
+------------------------------------------------------------------------+


🧪 Associated Hands-On Practical Lab

Practice the concepts from this masterclass in the accompanying hands-on Jupyter notebook: