ai newsMo., 26. Okt.
EilmeldungAnthropic vor Börsengang: Umsatz vervierzehnfacht und enorme KI-Kosten

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

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 re

dev.toMontag 15:19 · 26.10.13 Min. Lesezeit
Bild: dev.to

Posted on Oct 26 • Originally published at vericto.com

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. Enter fullscreen mode Exit fullscreen mode 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

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).

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).

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).

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.

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.

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

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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. Rule evaluation measured a p99 of 100 µs or less (staging, October 16, 2026), and the whole proxy added about 0.1 ms per statement at p50 (dedicated EC2, October 20–21, 2026; a measurement, not a guarantee: conditions and method). 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.

$ 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

=> 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.

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 six dialects: Postgres, MySQL, Oracle, SQL Server, Snowflake and SQLite. 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" } ],... } Enter fullscreen mode Exit fullscreen mode 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

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).

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).

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).

Tag your sensitive columns. Emails, cards, tokens: mask or block them so they never reach the model's context (12).

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).

Keep backups and point-in-time recovery where the agent's credentials can't delete them (2, 13).

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

Log every decision, including the refusals. What an agent tries after it is denied is exactly what you want on record.

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.

Quelle: dev.to (dev.to). Original öffnen