<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mauricio</title>
    <description>The latest articles on DEV Community by Mauricio (@mauricios89).</description>
    <link>https://dev.to/mauricios89</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4207494%2F7ec17177-0ef7-4684-a0f4-b6862b02a2b0.jpg</url>
      <title>DEV Community: Mauricio</title>
      <link>https://dev.to/mauricios89</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mauricios89"/>
    <language>en</language>
    <item>
      <title>Text-to-SQL guardrails: how to let an LLM query your database without letting it break it</title>
      <dc:creator>Mauricio</dc:creator>
      <pubDate>Wed, 28 Oct 2026 15:16:00 +0000</pubDate>
      <link>https://dev.to/mauricios89/text-to-sql-guardrails-how-to-let-an-llm-query-your-database-without-letting-it-break-it-lja</link>
      <guid>https://dev.to/mauricios89/text-to-sql-guardrails-how-to-let-an-llm-query-your-database-without-letting-it-break-it-lja</guid>
      <description>&lt;p&gt;Your team adds an assistant to the internal admin panel: someone types a question, an LLM turns it into SQL and the answer shows up in seconds. It works so well that people soon ask it for more than answers: “mark customer 42's March orders as refunded.” The model writes an UPDATE, loses the filter along the way, and this is the statement that reaches the database:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;UPDATE orders SET status = 'refunded';&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Nobody attacked anything, and the model means no harm. It wrote valid SQL for a request it half understood, and nothing between the model and the database checked the statement before it ran. The scenario is illustrative, but the pattern is documented in public cases.&lt;/p&gt;

&lt;p&gt;LangChain's documentation warns about it in its SQL agent tutorial: executing model-generated SQL has inherent risks, so scope the connection's permissions as narrowly as you can and add your own validation before that SQL runs (LangChain, “Build a SQL agent”). This article is about that validation: what it has to stop, which guardrails teams use today and where each one stops, and how to add it where your agent already runs SQL.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Where we stand. We build Vericto, database guardrails for AI agents: it checks every statement before it reaches the database. So we compare approaches, not vendors, and we also say what a SQL layer leaves out.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Four ways text-to-SQL goes wrong&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The ambiguous request. “Delete the test orders,” “update the price,” “clean up old sessions.” The model translates the intent and sometimes drops the condition: a DELETE or an UPDATE with no WHERE, or with a WHERE 1=1, which is the same thing. If the tool can write, every row in the table changes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The injected instruction. The text the model reads is not always written by your user: it can come in the question, in a ticket or in a database row. An instruction hidden there can turn a query into … OR 1=1, a DROP TABLE or a read of a table nobody asked for. This is not theoretical: LangChain's SQLDatabaseChain (CVE-2023-36189) and LlamaIndex's text-to-SQL engines (CVE-2024-23751) got CVEs for it, and researchers at INESC-ID characterized these “prompt-to-SQL” attacks on LangChain applications (arXiv 2308.01990).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The answer that exposes data. “Which customers complained last week?” To answer, the model reads names, emails and phone numbers, and those values land in its context, in the conversation logs and in the answer shown to whoever asked, whether or not they may see them. No destructive statement, no database permission broken, and it is still a data incident.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The unbounded read. A SELECT * with no LIMIT on the orders table works in development and returns the whole table in production: load on the database, a full context window and, again, far more data than the question needed.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The usual guardrails, and where each one stops&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;None of these approaches is useless. The question is which of the four failures each one stops.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7lrn8l1fdgtwer1ghu2b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7lrn8l1fdgtwer1ghu2b.png" alt=" " width="800" height="663"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Among the examples of LLM05:2025 (Improper Output Handling), OWASP also lists LLM-generated SQL queries executed without proper parameterization, leading to SQL injection. The combination that works is defense in depth: a least-privilege role underneath and, on top, a control that checks every statement outside the model and out of the agent's reach. Prompt instructions stay what they are: a way to help the model get it right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A guardrail the agent can use&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A control that only says “no” has a cost: the agent gets stuck, the user gets an opaque error, and someone ends up switching the guardrail off so the feature works again. What helps is a verdict the model can read: which rule the statement broke and how to write the safe version.&lt;/p&gt;

&lt;p&gt;This is what a LangChain agent gets back when Vericto blocks the UPDATE from the start. It is not an exception that ends the conversation: it is the SQL tool's result, in text the model understands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: Vericto blocked this query (VERICTO-030, severity critical). It was NOT executed.
Suggested fix: UPDATE orders SET {col} = $1 WHERE id = $2
Rewrite the query following the suggested fix and try again.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model tries again with the condition it had dropped, and this time the statement goes through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UPDATE orders SET status = 'refunded'
 WHERE customer_id = 42
   AND created_at &amp;gt;= '2026-03-01' AND created_at &amp;lt; '2026-04-01';
-- ALLOWED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same statement always gets the same verdict, because the decision comes from rules on the query's structure, not from a model. That lets you trust the control and, at the same time, explain every decision to whoever has to review it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How Vericto handles it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Vericto checks every SQL statement before it reaches the database: it parses it into a syntax tree, evaluates it against your workspace's rules and returns a verdict (ALLOWED, FLAGGED or BLOCKED) with the rule, the tree node that triggered it and a suggested fix. For text-to-SQL, that covers the four failures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Destructive statements are blocked. DELETE and UPDATE with no WHERE or an always-true one (VERICTO-001, 003, 030 and 042), DROP, TRUNCATE and the rest of the 42 standard rules, even when the statement comes after a COMMIT; in the same string.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Injection tautologies are blocked. An … OR 1=1 at any depth of the query is BLOCKED by VERICTO-090.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sensitive data comes back masked. In the dashboard you tag the columns that hold personal or payment data with mask, block or flag. With mask, the agent gets a***@example.com instead of the real email, and the value never enters the model's context (VERICTO-085).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Each agent stays within its allowlist. The tables and columns its task needs, read-only or read-write; everything else is denied with VERICTO-087. An injected instruction that asks for integration_tokens reads nothing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Unbounded reads are flagged. A SELECT with no LIMIT (VERICTO-050) or a SELECT * with no WHERE (VERICTO-051) run and are flagged, so you can see which agent reads whole tables.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Where your agent already runs SQL&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;In the agent's SQL tool: the Python and Node SDKs. vericto.langchain and vericto.llamaindex (Python 3.10+) and @vericto/sdk/langchain (LangChain.js) replace the framework's SQL database with one that asks Vericto before every execution. A block goes back to the model as the message above, and a masked column runs rewritten. Packages on PyPI and npm.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In any other stack: the Runtime API. Your code sends the query to POST /api/v1/proxy/query with one API key per agent and runs it only if the response allows it, using rewritten_query when there is one. On Postgres, MySQL, Oracle, SQL Server, Snowflake and SQLite.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In the connection: the TCP proxy. On Postgres and MySQL you change the host in the connection string and nothing else. It is the inline mode, so what it decides is what the database receives: a block arrives as a native driver error and a masked column arrives masked. If the database accepts connections only from the proxy, the agent has no way around the control.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;While you build it: MCP, the CLI and the GitHub Action. If a coding agent writes your feature's queries or migrations, connect it to the MCP server (&lt;a href="https://mcp.vericto.com/mcp" rel="noopener noreferrer"&gt;https://mcp.vericto.com/mcp&lt;/a&gt;) so it validates the SQL with check_sql before proposing it. The SQL that lands in the repository is checked on every pull request with vericto check or the vericto/check-sql@v1 action.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;In the dashboard. Every verdict lands in the audit trail with the rule that triggered it. That is where you tag sensitive columns, build allowlists and decide when to move from observing to blocking.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If Vericto does not answer, the SDK does not run the query: it fails closed by default, and with fail_mode="open" it runs it unevaluated and warns you. Parameter values are never sent to Vericto.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start by observing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A new guardrail on a feature people already use is scary for one reason: blocking something legitimate. That is why a workspace can start in observe mode: Vericto records everything and shows what it would have blocked, without blocking anything. After 7 days protection turns on by itself, or earlier, when you decide. Per-agent allowlists follow the same path: observe, a simulation against the agent's real queries, then enforce.&lt;/p&gt;

&lt;p&gt;The Free plan is permanent and needs no credit card: it includes the 42 standard rules, the Runtime API, the SDKs and the MCP server. Sensitive columns and per-agent allowlists are on Team and Enterprise, and Team has a 14-day trial.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before you ship text-to-SQL&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;A least-privilege role. Read-only if the feature only answers questions; never the application's credential or a superuser.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One identity per agent. Its own database user or API key, so you know who ran what and can give each one its allowlist.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Every statement checked before it runs, outside the model. On its structure, not its text, so WHERE 1=1 gets the same verdict as no WHERE at all.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sensitive columns masked or blocked before they reach the model's context.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Bounded reads. A LIMIT on what the agent reads, and only the tables the feature needs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A block the model can use. The rule and the suggested fix, so the agent corrects itself instead of getting stuck.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Every decision recorded, blocks included: what an agent tries after being told no is exactly what you want to see.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Try it on a staging database&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a free account and a workspace, and pick observe mode: nothing is blocked while you watch.&lt;/li&gt;
&lt;li&gt;Register the database your feature uses and create an API key for the agent with the proxy:evaluate scope.&lt;/li&gt;
&lt;li&gt;Install the SDK and swap the agent's database:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip install "vericto[langchain]"
export VERICTO_API_KEY=vtro_...            # API key with the proxy:evaluate scope
export VERICTO_DATABASE_ID=&amp;lt;database-id&amp;gt;   # the database you registered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from langchain_community.agent_toolkits import SQLDatabaseToolkit, create_sql_agent
from vericto.langchain import VerictoSQLDatabase

# Was: db = SQLDatabase.from_uri("postgresql+psycopg://app@db/app")
db = VerictoSQLDatabase.from_uri("postgresql+psycopg://app@db/app")
agent = create_sql_agent(llm, toolkit=SQLDatabaseToolkit(db=db, llm=llm))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Ask the agent for something ambiguous, such as “mark the orders as refunded,” and look at the verdict in the dashboard: the rule, the tree node and the suggested fix.&lt;/li&gt;
&lt;li&gt;When the log looks clean, turn protection on. On Team, tag the sensitive columns and give the agent its allowlist.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Using LlamaIndex, LangChain.js or another stack? The integration guide has the code for each, and the reference architecture for agents and text-to-SQL shows how to combine the proxy, the Runtime API and MCP.&lt;/p&gt;

&lt;p&gt;The goal is not to say no to text-to-SQL. It is to be able to say yes: let the model query real data knowing that every statement goes through a control that does not depend on the model getting it right.&lt;/p&gt;

</description>
      <category>texttosql</category>
      <category>ai</category>
      <category>database</category>
      <category>guardrail</category>
    </item>
    <item>
      <title>AI agents with database access: 13 documented cases and the controls that stop them</title>
      <dc:creator>Mauricio</dc:creator>
      <pubDate>Mon, 26 Oct 2026 14:19:00 +0000</pubDate>
      <link>https://dev.to/vericto/ai-agents-with-database-access-13-documented-cases-and-the-controls-that-stop-them-1dol</link>
      <guid>https://dev.to/vericto/ai-agents-with-database-access-13-documented-cases-and-the-controls-that-stop-them-1dol</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Nobody attacked anything. An agent tried to be useful with the access it had.&lt;/p&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How we built the list&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;No numbers we couldn't trace. Figures come from the sources, not from estimates.&lt;br&gt;
The 13 cases&lt;br&gt;
Agents running destructive SQL on their own&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;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).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;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).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;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).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;“Read-only” that wasn't&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Text-to-SQL tools that execute what the model writes&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Platforms and infrastructure&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;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.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;What keeps repeating&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;“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.&lt;/p&gt;

&lt;p&gt;Backups were missing, or sat next to the data (2, 13).&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Scorecard: would a check on each statement have stopped it?&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0lhmk61u54tf6a02ahit.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0lhmk61u54tf6a02ahit.png" alt=" " width="800" height="647"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How Vericto closes the gap&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;42 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:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;$ psql "$DATABASE_URL" -c "DELETE FROM users"&lt;br&gt;
ERROR:  Vericto blocked this query [VERICTO-001] — AST node: DeleteStmt &amp;gt; WhereClause = NULL. Suggestion: DELETE FROM users WHERE id = $1&lt;/code&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;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:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;code&gt;=&amp;gt; SELECT id, email, card_number FROM customers WHERE id = 42 LIMIT 1;&lt;br&gt;
 id |      email       | card_number&lt;br&gt;
----+------------------+-------------&lt;br&gt;
 42 | a***@example.com | ****4242&lt;br&gt;
(1 row)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;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).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;`=&amp;gt; SELECT * FROM integration_tokens;&lt;br&gt;
ERROR:  Vericto blocked this query [VERICTO-087] — AST node: AccessPolicy &amp;gt; integration_tokens (read)&lt;/p&gt;

&lt;p&gt;mysql&amp;gt; SELECT id FROM customers WHERE card_number LIKE '4%';&lt;br&gt;
ERROR 1142 (42000): Vericto blocked this query [VERICTO-087] — AST node: AccessPolicy &amp;gt; customers.card_number (read)`&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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).&lt;/p&gt;

&lt;p&gt;MySQL, read the way MySQL reads it (VERICTO-086)&lt;br&gt;
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.&lt;/p&gt;

&lt;p&gt;Where your agents already work: MCP, Runtime API and TCP proxy&lt;br&gt;
The same engine and the same workspace rules at every entry point:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;MCP. Connect Claude Code, Kiro, GitHub Copilot or another MCP client to the remote server at &lt;a href="https://mcp.vericto.com/mcp" rel="noopener noreferrer"&gt;https://mcp.vericto.com/mcp&lt;/a&gt;. 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.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;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).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ 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": "&amp;lt;database-id&amp;gt;", "dialect": "postgres"}'

# 403
{
  "error": "Query blocked by rule VERICTO-087",
  "decision": "BLOCKED",
  "rule_code": "VERICTO-087",
  "severity": "high",
  "ast_node_path": "AccessPolicy &amp;gt; integration_tokens (read)",
  "access_denied": [
    { "schema": null, "table": "integration_tokens", "column": null, "needed": "read" }
  ],
  ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you are giving agents database access&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;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).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;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).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;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).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Tag your sensitive columns. Emails, cards, tokens: mask or block them so they never reach the model's context (12).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;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).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Keep backups and point-in-time recovery where the agent's credentials can't delete them (2, 13).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;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 &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Log every decision, including the refusals. What an agent tries after it is denied is exactly what you want on record.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Don't count prompt instructions as controls. A code freeze in the prompt is a request, not a boundary (11, 13).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>mcp</category>
      <category>database</category>
      <category>cybersecurity</category>
    </item>
  </channel>
</rss>
