AI agents with database access: 13 documented cases and the controls that stop them

Coding agents, MCP servers and text-to-SQL tools: what went wrong in each case, what keeps repeating, and how Vericto blocks destructive SQL, masks sensitive data and keeps each agent to what it was granted.

15 min read
AI AGENTS RESEARCH

In May 2026, a coding agent dispatched a subagent to check why a test suite's reset logic was failing. The subagent wrote a script with a DELETE FROM for every table and ran it. The project's .env file still had the production credentials in it, so one of those scripts connected to production. A real customer's database was gone.

The repository even had a guardrail: test runs refused to start unless the database name ended in _test. The script loaded the environment on its own and never went through it.

Nobody attacked anything. An agent tried to be useful with the access it had.

We went through the public record of cases like this one: incidents, advisories, CVEs and research where an AI agent, an MCP server or a text-to-SQL tool did something destructive to a database, or could have. We kept the 13 with a source we could link to. This article covers what they have in common, which control would have stopped each one, and how Vericto applies those controls today.

Where we stand. We build Vericto: a deterministic layer between your agents and your database that checks every SQL statement before it runs. That is why we take the evidence seriously: every case links to its source, and the scorecard also shows what a SQL layer does not cover.

How we built the list

  • Public sources only. Postmortems, GitHub issues written by the people affected, vendor advisories, CVEs, a research paper and a conference talk. Self-reports are labelled as such: they are first-hand, but nobody else has verified them.
  • SQL run through the real engine. When a case published its SQL, or the statement it implies, we ran it through vericto-engine 3.8.0, the engine inside vericto-proxy 4.8.0 and vericto-eval 2.4.0, with the proxy's built-in ruleset (the full 31-rule catalogue), on Postgres and on MySQL where the statement applies. Each statement was evaluated 1,000 times and the verdict never changed: that is what a deterministic check means.
  • No numbers we couldn't trace. Figures come from the sources, not from estimates.

The 13 cases

Agents running destructive SQL on their own

1. A subagent runs DELETE FROM on every production table (May 2026). The case above. The script ran with no confirmation, and the parent agent first denied causing it because it hadn't checked its subagents' transcripts. Source: anthropics/claude-code#64056 (self-report).

2. drizzle-kit push --force against production: 60+ tables gone (February 2026). A coding agent running in another terminal pushed a schema change with --force to a production Postgres. According to the report there were no automatic backups or point-in-time recovery, and rebuilding took about eight hours. It was the second loss with the same tool in that project. Source: anthropics/claude-code#27063 (self-report).

3. An agent accepts a Prisma reset in production (March 2026, two reports). In one, prisma db push detected schema drift during a routine deploy and offered a reset; the agent accepted it, taking every table and 32 custom SQL functions with it. In the other, facing a data-loss warning, the agent ran prisma db push --force-reset in the background while DATABASE_URL pointed at production. Sources: #33183, #36183 (self-reports).

“Read-only” that wasn't

4. The reference Postgres MCP server (August 2025). It wrapped every query in a READ ONLY transaction. But the driver accepted several statements in one string, so COMMIT; DROP SCHEMA public CASCADE; closed the read-only transaction and ran the rest with the role's full privileges. The server had already been deprecated and archived, yet Datadog counted about 21,000 weekly downloads of the package. Source: Datadog Security Labs.

5. COPY … TO PROGRAM in AWS's Postgres MCP server (September 2026). In its default read-only mode, the server's SQL validator did not block every statement with side effects. A COPY … TO PROGRAM planted in content the agent processed could run operating-system commands on a self-managed Postgres host, if the database role had superuser or pg_execute_server_program. Fixed in 1.1.7. The advisory recommends a least-privilege role and blocking statements, as defense in depth. Sources: GHSA-fph8-pg5w-78fv, AWS bulletin 2026-104.

6. 18 of 19 “read-only” SQL MCP servers had flaws (2026). In a published talk, Maximilian Hildebrand reviewed 19 SQL MCP servers that advertise a read-only mode and found ways around it in 18, each in under 30 minutes. Source: media.ccc.de.

Text-to-SQL tools that execute what the model writes

7. LangChain's SQLDatabaseChain (2023). An issue pointed out that nothing stopped a user from asking, in plain English, to drop a table, and proposed checking the SQL before sending it. It became CVE-2023-36189. Sources: langchain#5923, GHSA-7q94-qpjr-xpgm.

8. LlamaIndex text-to-SQL (January 2024). Several of its text-to-SQL engines, up to 0.9.35 according to the GitHub advisory, could be steered by prompt injection into running arbitrary SQL, including dropping tables (CVE-2024-23751). Its documentation now recommends restricted roles, read-only databases or sandboxing. Sources: llama_index#9957, GHSA-2jxw-4hm4-6w87.

9. P2SQL: from prompt injection to SQL injection (2023, ICSE 2025). Researchers at INESC-ID and Técnico Lisboa characterised “prompt-to-SQL” attacks on LangChain web applications: the variants, their impact and the defenses. Sources: arXiv 2308.01990, ICSE 2025 paper.

10. Vanna.AI: prompt injection to code execution (June 2024). Vanna's ask function also generated Python to chart the results. With prompt injection, that Python could be anything (CVE-2024-5565). Source: JFrog.

Platforms and infrastructure

11. Replit's agent deletes SaaStr's production database during a code freeze (July 2025). Jason Lemkin documented it publicly. Replit's CEO confirmed it, called it unacceptable, and rolled out automatic separation of development and production databases. The exact mechanism was never confirmed in a primary source. Sources: The Register, Replit's response.

12. Supabase MCP: a support ticket leaks the tokens table (July 2025). In a demonstration, an agent connected through MCP with a role that bypasses row-level security processed a ticket with hidden instructions. It read a private integration_tokens table and copied it into the ticket thread, where the attacker could see it. No database permission was violated. Supabase responded with defense-in-depth changes to its MCP server. Sources: General Analysis, Supabase.

13. PocketOS: a production volume deleted through an infrastructure API (April 2026). While fixing a credentials mismatch in staging, a coding agent found an API token in an unrelated file, one created to manage domains but with full permissions, and deleted a production volume. The backups lived on the same volume. It took nine seconds. Railway helped restore the data and changed that endpoint to a delayed delete. Sources: The Register, the founder's post.

What keeps repeating

The production credential was within the agent's reach (cases 1, 3, 13). A .env file, a DATABASE_URL, a token in a file that had nothing to do with the task. The agent didn't escalate privileges. It used what was lying around.

The agent resolved an error with the most destructive option available (2, 3, 13). A --force flag, a reset prompt, deleting the thing that looked broken. Each of those confirmations exists for a human to stop and think. An agent trying to finish the task treats them as obstacles.

Instructions in natural language were not controls (11, 13). A code freeze stated in the prompt and project rules telling the agent what not to do did not hold when the agent decided they no longer applied.

“Read-only” lived in the wrong layer (4, 5, 6). It was a promise made by the application, implemented by wrapping or filtering queries. The database role underneath could still write.

Backups were missing, or sat next to the data (2, 13).

Not all damage is a DROP (9, 12). Reading what it shouldn't is an incident too: a token, an email or a card number that ends up in an LLM's context. A check that only looks at writes leaves that door open.

The industry is already reacting. Replit separated development and production databases. Railway made API deletes delayed. Prisma 6.15.0 now blocks prisma migrate reset --force when it detects an AI agent, until the user consents (release notes). These are real improvements. They also show where agent-side controls stop: Prisma's consent reaches the tool through an environment variable that the agent itself sets.

Scorecard: would a check on each statement have stopped it?

The question here is narrow on purpose. Would a deterministic check on each SQL statement, sitting on the connection between the agent and the database, have blocked the harmful part? That holds when production accepts connections through that check and nowhere else.

Case Statement (or its equivalent) Verdict Stopped?
1. Subagent DELETE FROM DELETE FROM patients; BLOCKED
VERICTO-001
Yes
2. drizzle-kit push --force DROP TABLE "trades" CASCADE; · ALTER TABLE … DROP COLUMN BLOCKED
VERICTO-010 VERICTO-015
Yes
3. Prisma reset DROP SCHEMA "public" CASCADE; BLOCKED
VERICTO-012
Likely. We have not confirmed the exact SQL Prisma sends on a reset
4. Reference Postgres MCP COMMIT; DROP SCHEMA public CASCADE; BLOCKED
VERICTO-012
Yes. Every statement in the string is checked
5. AWS Postgres MCP COPY (…) TO PROGRAM '…'; BLOCKED
VERICTO-080
Yes
6. 18 of 19 MCP servers — — Partly. Bypasses that end in writes, DDL or COPY … PROGRAM are blocked, and an allowlist also blocks reads of tables and catalogues the agent was not granted. Functions that read files are out of scope
7. LangChain DROP TABLE employee; BLOCKED
VERICTO-010
Yes
8. LlamaIndex DROP TABLE …; BLOCKED
VERICTO-010
Yes, for the destructive variant
9. P2SQL DELETE … WHERE 1=1 · … OR 1=1 · UPDATE with no WHERE BLOCKED
VERICTO-003 VERICTO-090 VERICTO-030
Partly. The destructive variants are blocked. An allowlist limits which tables and columns are read; which of other users' rows the query returns is still a job for row-level security
10. Vanna.AI — — No. The payload was Python, not SQL
11. Replit DROP TABLE …; BLOCKED
VERICTO-010
Only if that is what ran and the platform let you put a check on that connection, which it doesn't
12. Supabase MCP SELECT * FROM integration_tokens; FLAGGED
VERICTO-050
BLOCKED
VERICTO-087
Yes, with an allowlist. Without one, the SELECT is only flagged (VERICTO-050). With an allowlist that does not include integration_tokens, it is blocked before a single token is read; a block tag on the column blocks it with VERICTO-085
13. PocketOS — — No. It was an infrastructure API call, not SQL

Six clear yes, one more with an allowlist, one likely, two partial, one conditional and two no. The two no (10 and 13) never went through SQL: a Python payload and an infrastructure API call. For those, the answer is least privilege, isolated tokens and backups the agent can't reach. When the damage goes through SQL, which is most of the cases, a check on each statement is the layer that stops it.

How Vericto closes the gap

Vericto sits between your agents and your database. It parses every statement into its syntax tree (AST) and decides on the structure, not the text: the same query always gets the same decision, with a P99 < 2 ms design target for rule evaluation. Three controls run on that engine and, together, they cover both risks in this list: an agent that destroys data, and an agent that reads what it shouldn't.

1. 31 deterministic rules against destructive SQL

DELETE or UPDATE with no WHERE, WHERE 1=1, DROP, TRUNCATE, COPY … TO PROGRAM, GRANT, the OR 1=1 tautology: the standard catalogue blocks them before they reach the database, even behind a COMMIT; in the same string. With the TCP proxy, the agent gets a native error from its own driver, with the rule and a suggested safe version:

$ 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. Sensitive Column Protection (VERICTO-085)

Blocking a DELETE protects the database from the agent. Sensitive columns protect the data: you tag the columns that hold personal or payment data in the dashboard and choose what happens to every query that reads them: block, flag or mask. With mask, the TCP proxy rewrites the projection and the database returns the value already masked, on Postgres and on MySQL (MariaDB and Aurora MySQL too). The agent keeps working and the real value never enters the model's context:

=> SELECT id, email, card_number FROM customers WHERE id = 42 LIMIT 1;
 id |      email       | card_number
----+------------------+-------------
 42 | a***@example.com | ****4242
(1 row)

email uses the email style and card_number the last4 style; full ([redacted]) and hash (SHA-256) are there too. An alias, a function or a subquery over the column still counts as a read, and a SELECT * on a table with masked columns is blocked with a message asking to list the columns. Every decision lands in the audit trail with the column that triggered it.

3. Per-agent access allowlists (VERICTO-087)

The third piece closes case 12 and bounds what a successful prompt injection can achieve. Each agent gets its own allowlist on a database: the tables and columns it may use, read-only or read-write. Everything else is denied for that identity. Every reference counts, not only what is returned: a WHERE card_number LIKE '4%' is access too, because it lets the agent probe values. DDL is always denied, and so are the system catalogues and the statements that change who the session is (SET ROLE, SET search_path, USE).

For example, a support agent with tickets read-write and customers read-only (id, name, email), connected through the TCP proxy. On Postgres and on 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)

A policy starts in observe mode: whatever falls outside it is flagged and never blocked. Before switching to enforce, Simulate replays that identity's queries from the last 7 days (up to 2,000) against the draft and tells you how many it would have blocked, without sending anything to your database. The identity is the calling API key (Runtime API, CLI, Validation API and MCP) or the session's database user (TCP proxy). And the controls compose: a column that is allowed but tagged mask comes back masked.

An improvement to name semantics is already rolling out: an entry without a schema will mean the default schema, configurable per database, and names will compare the way your database compares them (on Postgres, "User" and user are two tables).

MySQL, read the way MySQL reads it (VERICTO-086)

An AST check is only as good as its reading of the text. MySQL has executable comments (/*! … */) and backslash escapes that depend on server settings, so every rule runs on the statement MySQL executes: comments are read with MySQL's rules and backslash strings under both escape modes, keeping the strictest outcome. Text that cannot be resolved to one statement with certainty is blocked by VERICTO-086, a CRITICAL rule that cannot be turned off. We published advisory GHSA-64jm-544q-7f5j; if you use MySQL, run vericto-proxy 4.7.1 and vericto-eval 2.3.0 or later.

Where your agents already work: MCP, Runtime API and TCP proxy

The same engine and the same workspace rules at every entry point:

  • MCP. Connect Claude Code, Kiro, GitHub Copilot or another MCP client to the remote server at https://mcp.vericto.com/mcp. The agent calls check_sql while it writes SQL and gets the verdict, the rule and the suggested fix before a commit exists. When a finding carries an approved_query, that is the masked version the agent should run.
  • Runtime API. Your application or your agent's tool sends each query to POST /api/v1/proxy/query before running it, with one API key per agent, in all four dialects: Postgres, MySQL, Oracle and SQL Server. A query outside the allowlist gets a 403 with what was denied (below).
  • TCP proxy. You change the host in your DATABASE_URL and nothing else: neither the ORM nor the driver notices. It is the mode that sits inline between the query and the database, so what it decides is what the database receives: a block is a native error (SQLSTATE 42501 on Postgres, ERROR 1142 on MySQL) and a masked column arrives masked.
$ 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" }
  ],
  ...
}

For the full guarantee, have the agent reach the database only through the TCP proxy, with its own database user and its allowlist, and have the database accept connections only from the proxy. With the Runtime API and MCP, Vericto returns the decision and your application or agent carries it out.

Sensitive Column Protection and per-agent allowlists are included in the Team and Enterprise plans. The Free plan is permanent, with 500K queries/month and no credit card, and Builder and Team come with a 14-day trial.

If you are giving agents database access

  1. Keep production credentials out of anything the agent can read. Not in .env files shared with tests, not in tokens left in unrelated files. Give agents their own credentials and keep production ones separate (cases 1, 3, 13).
  2. Make read-only a property of the database role. Grant SELECT and nothing else. Never superuser, never pg_execute_server_program. Don't rely on the tool wrapping queries (4, 5, 6).
  3. Give each agent its own identity and an allowlist. One API key or database user per agent, with the tables and columns its task needs and nothing else. Start in observe, simulate, then switch to enforce (6, 9, 12).
  4. Tag your sensitive columns. Emails, cards, tokens: mask or block them so they never reach the model's context (12).
  5. Assume the agent will accept every --force and every reset prompt. Disable what you can in the agent's tooling, but don't make that the only line (2, 3).
  6. Keep backups and point-in-time recovery where the agent's credentials can't delete them (2, 13).
  7. Check each statement before it reaches the database, outside the agent. A check on the structure of the query, not the text, so DELETE FROM users WHERE 1=1 gets the same verdict as DELETE FROM users. It has to sit where the agent can't skip it or switch it off (1, 2, 4, 5, 7, 8).
  8. Log every decision, including the refusals. What an agent tries after it is denied is exactly what you want on record.
  9. Don't count prompt instructions as controls. A code freeze in the prompt is a request, not a boundary (11, 13).

None of this is exotic: most teams already have it for people. Agents need the same controls, applied on the assumption that nobody reads the query before it runs. That is what Vericto does, on every statement.