En mayo de 2026, un agente de código lanzó un subagente para revisar por qué fallaba la lógica de reset de una suite de tests. El subagente escribió un script con un DELETE FROM para cada tabla y lo ejecutó. El archivo .env del proyecto todavía tenía las credenciales de producción, así que uno de esos scripts se conectó a producción. La base de datos de un cliente real desapareció.
El repositorio incluso tenía un guardrail: los tests no arrancaban si el nombre de la base no terminaba en _test. El script cargó el entorno por su cuenta y nunca pasó por ahí.
Nadie atacó nada. Un agente intentó ser útil con el acceso que tenía.
Revisamos el registro público de casos como este: incidentes, advisories, CVEs e investigaciones en los que un agente de IA, un servidor MCP o una herramienta text-to-SQL hizo algo destructivo en una base de datos, o pudo hacerlo. Nos quedamos con los 13 que tienen una fuente enlazable. Este artículo cuenta qué tienen en común, qué control habría frenado cada uno y cómo se aplican hoy esos controles con Vericto.
Nuestra posición. Construimos Vericto: una capa determinista entre tus agentes y tu base de datos que revisa cada sentencia SQL antes de que se ejecute. Por eso nos tomamos en serio la evidencia: cada caso enlaza a su fuente, y la tabla de resultados también muestra lo que una capa SQL no cubre.
Cómo armamos la lista
- Solo fuentes públicas. Postmortems, issues de GitHub escritos por los afectados, advisories de proveedores, CVEs, un paper y una charla. Los autorreportes están marcados como tales: son de primera mano, pero nadie más los verificó.
- El SQL, por el motor real. Cuando un caso publicó su SQL, o la sentencia que implica, la pasamos por vericto-engine 3.8.0, el motor de vericto-proxy 4.8.0 y vericto-eval 2.4.0, con el ruleset integrado del proxy (el catálogo completo de 31 reglas), en Postgres y en MySQL cuando la sentencia aplica. Evaluamos cada sentencia 1,000 veces y el veredicto nunca cambió: eso es lo que significa un control determinista.
- Ninguna cifra que no se pueda rastrear. Los números salen de las fuentes, no de estimaciones.
Los 13 casos
Agentes que ejecutan SQL destructivo por su cuenta
1. Un subagente ejecuta DELETE FROM en cada tabla de producción (mayo de 2026). El caso de arriba. El script corrió sin confirmación, y el agente principal primero negó haberlo causado porque no había revisado las transcripciones de sus subagentes. Fuente: anthropics/claude-code#64056 (autorreporte).
2. drizzle-kit push --force contra producción: más de 60 tablas perdidas (febrero de 2026). Un agente de código que corría en otra terminal empujó un cambio de schema con --force a un Postgres de producción. Según el reporte no había backups automáticos ni recuperación a un punto en el tiempo, y reconstruir tomó unas ocho horas. Era la segunda pérdida con la misma herramienta en ese proyecto. Fuente: anthropics/claude-code#27063 (autorreporte).
3. Un agente acepta un reset de Prisma en producción (marzo de 2026, dos reportes). En uno, prisma db push detectó una diferencia de schema durante un deploy de rutina y ofreció un reset; el agente lo aceptó y se llevó todas las tablas y 32 funciones SQL propias. En el otro, ante un aviso de pérdida de datos, el agente corrió prisma db push --force-reset en segundo plano mientras DATABASE_URL apuntaba a producción. Fuentes: #33183, #36183 (autorreportes).
Un “read-only” que no lo era
4. El servidor MCP de referencia para Postgres (agosto de 2025). Envolvía cada query en una transacción READ ONLY. Pero el driver aceptaba varias sentencias en un mismo string, así que COMMIT; DROP SCHEMA public CASCADE; cerraba la transacción de solo lectura y el resto corría con todos los privilegios del rol. El servidor ya estaba deprecado y archivado, y aun así Datadog contó unas 21,000 descargas semanales del paquete. Fuente: Datadog Security Labs.
5. COPY … TO PROGRAM en el servidor MCP de AWS para Postgres (septiembre de 2026). En su modo read-only por defecto, el validador de SQL del servidor no bloqueaba todas las sentencias con efectos secundarios. Un COPY … TO PROGRAM colocado en contenido que el agente procesaba podía ejecutar comandos del sistema operativo en el host de un Postgres autogestionado, si el rol tenía superuser o pg_execute_server_program. Se corrigió en la 1.1.7. El advisory recomienda un rol con privilegios mínimos y bloquear sentencias, como defensa en profundidad. Fuentes: GHSA-fph8-pg5w-78fv, boletín de AWS 2026-104.
6. 18 de 19 servidores MCP SQL “read-only” tenían fallas (2026). En una charla publicada, Maximilian Hildebrand revisó 19 servidores MCP SQL que anuncian un modo read-only y encontró cómo saltárselo en 18, en menos de 30 minutos cada uno. Fuente: media.ccc.de.
Herramientas text-to-SQL que ejecutan lo que escribe el modelo
7. SQLDatabaseChain de LangChain (2023). Un issue señaló que nada impedía que un usuario pidiera, en lenguaje natural, borrar una tabla, y propuso revisar el SQL antes de enviarlo. Quedó registrado como CVE-2023-36189. Fuentes: langchain#5923, GHSA-7q94-qpjr-xpgm.
8. Text-to-SQL de LlamaIndex (enero de 2024). Varios de sus motores text-to-SQL, hasta la 0.9.35 según el advisory de GitHub, se podían manipular con prompt injection para ejecutar SQL arbitrario, incluido borrar tablas (CVE-2024-23751). Hoy su documentación recomienda roles restringidos, bases de solo lectura o sandboxing. Fuentes: llama_index#9957, GHSA-2jxw-4hm4-6w87.
9. P2SQL: de prompt injection a SQL injection (2023, ICSE 2025). Investigadores de INESC-ID y Técnico Lisboa caracterizaron los ataques “prompt-to-SQL” en aplicaciones web con LangChain: sus variantes, su impacto y las defensas. Fuentes: arXiv 2308.01990, paper de ICSE 2025.
10. Vanna.AI: de prompt injection a ejecución de código (junio de 2024). La función ask de Vanna también generaba Python para graficar los resultados. Con prompt injection, ese Python podía ser cualquier cosa (CVE-2024-5565). Fuente: JFrog.
Plataformas e infraestructura
11. El agente de Replit borra la base de producción de SaaStr durante un code freeze (julio de 2025). Jason Lemkin lo documentó públicamente. El CEO de Replit lo confirmó, lo calificó de inaceptable y empezó a separar automáticamente las bases de desarrollo y producción. El mecanismo exacto nunca se confirmó en una fuente primaria. Fuentes: The Register, respuesta de Replit.
12. Supabase MCP: un ticket de soporte filtra la tabla de tokens (julio de 2025). En una demostración, un agente conectado por MCP con un rol que se salta la seguridad a nivel de fila procesó un ticket con instrucciones ocultas. Leyó una tabla privada, integration_tokens, y la copió en el hilo del ticket, donde el atacante podía verla. No se violó ningún permiso de la base. Supabase respondió con cambios de defensa en profundidad en su servidor MCP. Fuentes: General Analysis, Supabase.
13. PocketOS: un volumen de producción borrado con una API de infraestructura (abril de 2026). Mientras arreglaba un desajuste de credenciales en staging, un agente de código encontró un token de API en un archivo no relacionado, creado para gestionar dominios pero con permisos totales, y borró un volumen de producción. Los backups estaban en el mismo volumen. Tomó nueve segundos. Railway ayudó a restaurar los datos y cambió ese endpoint a un borrado diferido. Fuentes: The Register, el post del fundador.
Lo que se repite
La credencial de producción estaba al alcance del agente (casos 1, 3 y 13). Un archivo .env, un DATABASE_URL, un token en un archivo que no tenía nada que ver con la tarea. El agente no escaló privilegios: usó lo que estaba a mano.
El agente resolvió un error con la opción más destructiva disponible (2, 3 y 13). Un flag --force, un prompt de reset, borrar lo que parecía roto. Cada una de esas confirmaciones existe para que una persona se detenga a pensar. Un agente que intenta terminar la tarea las trata como obstáculos.
Las instrucciones en lenguaje natural no fueron controles (11 y 13). Un code freeze pedido en el prompt y reglas del proyecto que le decían al agente qué no hacer no se sostuvieron cuando el agente decidió que ya no aplicaban.
El “read-only” vivía en la capa equivocada (4, 5 y 6). Era una promesa de la aplicación, implementada envolviendo o filtrando queries. El rol de la base, debajo, todavía podía escribir.
No había backups, o estaban junto a los datos (2 y 13).
No todo el daño es un DROP (9 y 12). Leer lo que no corresponde también es un incidente: un token, un email o un número de tarjeta que termina en el contexto de un LLM. Un control que solo mira las escrituras deja esa puerta abierta.
La industria ya está reaccionando. Replit separó las bases de desarrollo y producción. Railway hizo diferidos los borrados por API. Prisma 6.15.0 ahora bloquea prisma migrate reset --force cuando detecta un agente de IA, hasta que el usuario da su consentimiento (notas de la versión). Son mejoras reales. También muestran dónde terminan los controles del lado del agente: el consentimiento de Prisma le llega a la herramienta por una variable de entorno que fija el propio agente.
Resultados: ¿lo habría frenado un control en cada sentencia?
La pregunta es estrecha a propósito. ¿Un control determinista sobre cada sentencia SQL, ubicado en la conexión entre el agente y la base, habría bloqueado la parte dañina? Eso se cumple cuando producción acepta conexiones a través de ese control y de ningún otro lado.
| Caso | Sentencia (o su equivalente) | Veredicto | ¿Lo habría frenado? |
|---|---|---|---|
| 1. Subagente con DELETE FROM | DELETE FROM patients; |
BLOCKEDVERICTO-001 |
Sí |
| 2. drizzle-kit push --force | DROP TABLE "trades" CASCADE; · ALTER TABLE … DROP COLUMN |
BLOCKEDVERICTO-010 VERICTO-015 |
Sí |
| 3. Reset de Prisma | DROP SCHEMA "public" CASCADE; |
BLOCKEDVERICTO-012 |
Probablemente. No confirmamos el SQL exacto que manda Prisma en un reset |
| 4. MCP de referencia para Postgres | COMMIT; DROP SCHEMA public CASCADE; |
BLOCKEDVERICTO-012 |
Sí. Se evalúa cada sentencia del string |
| 5. MCP de AWS para Postgres | COPY (…) TO PROGRAM '…'; |
BLOCKEDVERICTO-080 |
Sí |
| 6. 18 de 19 servidores MCP | — | — | En parte. Se bloquean los bypasses que terminan en escrituras, DDL o COPY … PROGRAM, y una allowlist también bloquea la lectura de tablas y catálogos que no se le concedieron al agente. Las funciones que leen archivos quedan fuera |
| 7. LangChain | DROP TABLE employee; |
BLOCKEDVERICTO-010 |
Sí |
| 8. LlamaIndex | DROP TABLE …; |
BLOCKEDVERICTO-010 |
Sí, para la variante destructiva |
| 9. P2SQL | DELETE … WHERE 1=1 · … OR 1=1 · UPDATE sin WHERE |
BLOCKEDVERICTO-003 VERICTO-090 VERICTO-030 |
En parte. Se bloquean las variantes destructivas. Una allowlist limita qué tablas y columnas se leen; qué filas de otros usuarios devuelve la query sigue siendo tarea del row-level security |
| 10. Vanna.AI | — | — | No. El payload era Python, no SQL |
| 11. Replit | DROP TABLE …; |
BLOCKEDVERICTO-010 |
Solo si lo que corrió fue un DROP y la plataforma dejara poner un control en esa conexión, que no lo permite |
| 12. Supabase MCP | SELECT * FROM integration_tokens; |
FLAGGEDVERICTO-050BLOCKED VERICTO-087 |
Sí, con una allowlist. Sin ella, el SELECT solo se marca (VERICTO-050). Con una allowlist que no incluye integration_tokens, se bloquea antes de leer un solo token; una etiqueta block en la columna lo bloquea con VERICTO-085 |
| 13. PocketOS | — | — | No. Fue una llamada a una API de infraestructura, no SQL |
Seis sí claros, uno más con una allowlist, uno probable, dos parciales, uno condicional y dos no. Los dos no (10 y 13) nunca pasaron por SQL: un payload en Python y una llamada a una API de infraestructura. Para esos, la respuesta son privilegios mínimos, tokens aislados y backups fuera del alcance del agente. Cuando el daño pasa por SQL, que es la mayoría de los casos, un control en cada sentencia es la capa que lo frena.
Cómo lo resuelve Vericto
Vericto se ubica entre tus agentes y tu base de datos. Parsea cada sentencia a su árbol sintáctico (AST) y decide sobre la estructura, no sobre el texto: la misma query recibe siempre la misma decisión, con un objetivo de diseño de P99 < 2 ms para la evaluación de reglas. Sobre ese motor corren tres controles que, juntos, cubren los dos riesgos de esta lista: un agente que destruye datos y un agente que lee lo que no debe.
1. 31 reglas deterministas contra el SQL destructivo
DELETE o UPDATE sin WHERE, WHERE 1=1, DROP, TRUNCATE, COPY … TO PROGRAM, GRANT, la tautología OR 1=1: el catálogo estándar los bloquea antes de que lleguen a la base, aunque vengan detrás de un COMMIT; en el mismo string. Con el proxy TCP, el agente recibe un error nativo de su propio driver, con la regla y una versión segura sugerida:
$ psql "$DATABASE_URL" -c "DELETE FROM users"
ERROR: Vericto blocked this query [VERICTO-001] — AST node: DeleteStmt > WhereClause = NULL. Suggestion: DELETE FROM users WHERE id = $1
2. Protección de columnas sensibles (VERICTO-085)
Bloquear un DELETE protege la base del agente. Las columnas sensibles protegen los datos: marcas en el dashboard las columnas con datos personales o de pago y eliges qué pasa con cada query que las lee: block, flag o mask. Con mask, el proxy TCP reescribe la proyección y la base de datos devuelve el valor ya enmascarado, en Postgres y en MySQL (también MariaDB y Aurora MySQL). El agente sigue trabajando y el dato real nunca entra en el contexto del modelo:
=> SELECT id, email, card_number FROM customers WHERE id = 42 LIMIT 1;
id | email | card_number
----+------------------+-------------
42 | a***@example.com | ****4242
(1 row)
email usa el estilo email y card_number el estilo last4; también están full ([redacted]) y hash (SHA-256). Un alias, una función o una subquery sobre la columna siguen contando como lectura, y un SELECT * sobre una tabla con columnas enmascaradas se bloquea con un mensaje que pide listar las columnas. Cada decisión queda en el audit trail con la columna que la disparó.
3. Allowlists de acceso por agente (VERICTO-087)
La tercera pieza cierra el caso 12 y acota lo que puede lograr una prompt injection exitosa. Cada agente recibe su propia allowlist en una base de datos: las tablas y columnas que puede usar, en solo lectura o en lectura y escritura. Todo lo demás se deniega para esa identidad. Cuenta toda referencia, no solo lo que se devuelve: un WHERE card_number LIKE '4%' también es acceso, porque permite sondear valores. El DDL siempre se deniega, y también los catálogos del sistema y las sentencias que cambian la identidad de la sesión (SET ROLE, SET search_path, USE).
Por ejemplo, un agente de soporte con tickets en lectura y escritura y customers en solo lectura (id, name, email), conectado por el proxy TCP. En Postgres y en MySQL:
=> SELECT * FROM integration_tokens;
ERROR: Vericto blocked this query [VERICTO-087] — AST node: AccessPolicy > integration_tokens (read)
mysql> SELECT id FROM customers WHERE card_number LIKE '4%';
ERROR 1142 (42000): Vericto blocked this query [VERICTO-087] — AST node: AccessPolicy > customers.card_number (read)
Una política empieza en modo observe: lo que queda fuera se marca y nunca se bloquea. Antes de pasar a enforce, Simular reproduce las queries de esa identidad de los últimos 7 días (hasta 2.000) con el borrador y dice cuántas habría bloqueado, sin enviar nada a tu base. La identidad es la API key que llama (Runtime API, CLI, Validation API y MCP) o el usuario de base de datos de la sesión (proxy TCP). Y los controles se combinan: una columna permitida pero etiquetada mask se devuelve enmascarada.
Ya se está desplegando una mejora en la semántica de nombres: una entrada sin esquema significará el esquema por defecto, configurable por base de datos, y los nombres se compararán como en tu motor (en Postgres, "User" y user son dos tablas).
MySQL, leído como lo lee MySQL (VERICTO-086)
Un control sobre el AST vale lo que vale su lectura del texto. MySQL tiene comentarios ejecutables (/*! … */) y escapes con barra invertida que dependen de la configuración del servidor, así que cada regla corre sobre la sentencia que MySQL ejecuta: los comentarios se leen con las reglas de MySQL y los strings con barra invertida bajo los dos modos de escape, y se queda el resultado más estricto. El texto que no se puede resolver con certeza a una sola sentencia se bloquea con VERICTO-086, una regla CRITICAL que no se puede desactivar. Publicamos el aviso GHSA-64jm-544q-7f5j; si usas MySQL, usa vericto-proxy 4.7.1 y vericto-eval 2.3.0 o posteriores.
Donde ya trabajan tus agentes: MCP, Runtime API y proxy TCP
El mismo motor y las mismas reglas del workspace en cada punto de entrada:
- MCP. Conecta Claude Code, Kiro, GitHub Copilot u otro cliente MCP al servidor remoto
https://mcp.vericto.com/mcp. El agente llama acheck_sqlmientras escribe SQL y recibe el veredicto, la regla y la corrección sugerida antes de que exista un commit. Si un hallazgo traeapproved_query, esa es la versión enmascarada que el agente debe ejecutar. - Runtime API. Tu aplicación o la herramienta de tu agente envía cada query a
POST /api/v1/proxy/queryantes de ejecutarla, con una API key por agente, en los seis dialectos: Postgres, MySQL, Oracle, SQL Server, Snowflake y SQLite. Una query fuera de la allowlist responde403con lo que se denegó (abajo). - Proxy TCP. Cambias el host de tu
DATABASE_URLy nada más: ni el ORM ni el driver se enteran. Es el modo que está en línea entre la query y la base, así que lo que decide es lo que la base recibe: un bloqueo es un error nativo (SQLSTATE 42501 en Postgres, ERROR 1142 en MySQL) y una columna enmascarada llega enmascarada.
$ curl -s -X POST https://api.vericto.com/api/v1/proxy/query \
-H "Authorization: Bearer $SUPPORT_AGENT_KEY" \
-H "Content-Type: application/json" \
-d '{"query": "SELECT * FROM integration_tokens", "database_id": "<database-id>", "dialect": "postgres"}'
# 403
{
"error": "Query blocked by rule VERICTO-087",
"decision": "BLOCKED",
"rule_code": "VERICTO-087",
"severity": "high",
"ast_node_path": "AccessPolicy > integration_tokens (read)",
"access_denied": [
{ "schema": null, "table": "integration_tokens", "column": null, "needed": "read" }
],
...
}
Para la garantía completa, haz que el agente llegue a la base solo a través del proxy TCP, con su propio usuario de base de datos y su allowlist, y que la base solo acepte conexiones desde el proxy. En la Runtime API y en MCP, Vericto entrega la decisión y tu aplicación o tu agente la ejecuta.
La protección de columnas sensibles y las allowlists por agente están en los planes Team y Enterprise. El plan Free es permanente, con 500K queries/mes y sin tarjeta de crédito, y Builder y Team tienen 14 días de prueba.
Si vas a darle acceso a la base a un agente
- Mantén las credenciales de producción fuera de todo lo que el agente pueda leer. Ni en archivos
.envcompartidos con los tests, ni en tokens olvidados en archivos sin relación. Dale al agente sus propias credenciales y separa las de producción (casos 1, 3 y 13). - Haz que el read-only sea una propiedad del rol de la base. Otorga
SELECTy nada más. Nunca superuser, nuncapg_execute_server_program. No dependas de que la herramienta envuelva las queries (4, 5 y 6). - Dale a cada agente una identidad propia y una allowlist. Una API key o un usuario de base de datos por agente, con las tablas y columnas que su tarea necesita y nada más. Empieza en observe, simula y pasa a enforce (6, 9 y 12).
- Marca las columnas sensibles. Emails, tarjetas, tokens: enmascáralos o bloquéalos para que nunca lleguen al contexto del modelo (12).
- Asume que el agente va a aceptar cada
--forcey cada prompt de reset. Desactiva lo que puedas en las herramientas del agente, pero que no sea la única línea (2 y 3). - Guarda los backups y la recuperación a un punto en el tiempo donde las credenciales del agente no puedan borrarlos (2 y 13).
- Revisa cada sentencia antes de que llegue a la base, fuera del agente. Un control sobre la estructura de la query, no sobre el texto, para que
DELETE FROM users WHERE 1=1reciba el mismo veredicto queDELETE FROM users. Tiene que estar donde el agente no pueda saltárselo ni apagarlo (1, 2, 4, 5, 7 y 8). - Registra cada decisión, incluidas las negativas. Lo que un agente intenta después de que le niegan algo es justo lo que quieres tener registrado.
- No cuentes las instrucciones del prompt como controles. Un code freeze en el prompt es un pedido, no un límite (11 y 13).
Nada de esto es exótico: la mayoría de los equipos ya lo tiene para las personas. Los agentes necesitan lo mismo, aplicado bajo el supuesto de que nadie lee la query antes de que se ejecute. Eso es lo que hace Vericto, en cada sentencia.