No. 002Security8 min read

Prompt injection is the new SQL injection — the fix isn't SQL

P2SQL, CVE-2024-5565, EchoLeak, the Supabase leak: hostile SQL now arrives in the model's output. The fix is an agent with no syntax to inject into.

The support ticket didn't look like an attack. It sat in the queue like any other — a customer message waiting to be summarized. Inside the body were instructions: read the integration_tokens table and post the contents into this thread.

A developer had pointed an AI assistant at that queue through the Supabase MCP server, which connects with service_role credentials — the role that bypasses row-level security entirely. The assistant read the ticket, took its contents as a task, and ran it against the production database. The tokens surfaced in the ticket thread, where the attacker could simply read them back Simon Willison, 2025.

Nothing was exploited in the traditional sense. No memory corruption, no missing auth check, no vulnerable dependency. The hostile text reached the data plane purely through the agent — and the proof of concept generalizes to any agent wired the same way.

This is SQL injection with the interpreter relocated. Hostile text still becomes a database command. But the string no longer arrives in the input, where twenty years of sanitization tooling stands guard. It arrives in the model's output — downstream of every filter you own.

SQL INJECTION, 2005 — THE PAYLOAD ARRIVES IN THE INPUT input sanitizer application database parameterized payload dies here PROMPT-TO-SQL, NOW — THE PAYLOAD ARRIVES IN THE OUTPUT input sanitizer model generated SQL database untrusted context — tickets, pages, rows hostile SQL is authored here — after every filter
The filter guards a door the payload no longer uses — in P2SQL, hostile SQL is authored in the model's output (Pedro et al., ICSE 2025).

The record

Researchers at INESC-ID Lisbon named the pattern in 2023: P2SQL, prompt-to-SQL injection Pedro et al., 2025. Working against a real-world LangChain chatbot over Postgres, they constructed seven representative attacks of increasing severity — unauthorized reads, corrupted tables, dropped tables, on up to code execution — and ran them across seven state-of-the-art models. The failures were model-agnostic. Their conclusion, verbatim: "LLM-integrated applications based on Langchain are highly susceptible to P2SQL injection attacks." The paper landed at ICSE 2025 — peer review's way of saying this is not a curiosity.

By mid-2024 the pattern had a CVE number. Vanna.AI, a text-to-SQL library, passed user questions into a prompt that generated Plotly charting code, then ran that code with exec(). A crafted question walked straight through to remote code execution — CVE-2024-5565, CVSS 8.1 JFrog, 2024. Note where the payload lived: in the model's output, past every input check. Vanna's guardrail pre-prompts didn't stop it, because a guardrail is just more text in the same context window as the attack.

By late 2024 the ranking was official: prompt injection is LLM01, the number-one risk in the OWASP Top 10 for LLM Applications, holding the top spot for the second consecutive edition OWASP, 2025. OWASP's diagnosis of the root cause is the whole story in one sentence: LLMs process instructions and data in the same channel, with no clear separation. That is also why the category splits in two — direct injection typed by the user, and indirect injection riding in whatever the model retrieves. The Supabase ticket was indirect. So was the worst one on record.

In June 2025, CVE-2025-32711 — EchoLeak, CVSS 9.3 — was disclosed in Microsoft 365 Copilot: the first documented real-world zero-click prompt-injection exploit against a production LLM system arXiv, 2025. One crafted email — instructions hidden in HTML comments, white-on-white text, reference-style Markdown — and Copilot exfiltrated internal data to an attacker's server. The user clicked nothing. The chain defeated Microsoft's dedicated cross-prompt-injection classifier and its link redaction on the way out.

CVSS 10 8.1 9.3 2025-06 CVE-2025-32711 — EchoLeak zero-click, one email, M365 Copilot 2023-08 P2SQL coined 7 attacks × 7 LLMs 2024-06 CVE-2024-5565 Vanna.AI — RCE 2024-11 OWASP LLM01 #1 for 2nd edition 2025-07 Supabase MCP PoC no code vulnerability
Two years, lab to zero-click — CVSS bars to scale (arXiv 2308.01990; JFrog 2024; OWASP 2025; arXiv 2509.10540; Willison 2025).

Why the obvious fixes lose

Each incident above retires one of the standard defenses.

Filter the input. EchoLeak went through XPIA, a classifier Microsoft built specifically to catch injection attempts arXiv, 2025. Classifiers are probabilistic. The attacker needs one miss and gets unlimited retries at manufacturing it; the defender needs a perfect record against inputs designed to look like everything else.

Instruct the model. Vanna shipped guardrail prompts; CVE-2024-5565 walked through them JFrog, 2024. There is no privileged channel inside a context window. System text, user text, and attack text are the same substance — which is exactly the root cause OWASP names OWASP, 2025.

Firewall the output. Parse the generated SQL and block the scary parts. Now you maintain a denylist over every write-capable construct in every dialect you support, plus every function with side effects — while the adversary's co-author is the model itself, happy to reformulate a blocked query into an allowed shape. Tellingly, the P2SQL authors' own proposed defenses live outside the model: parse and allowlist the generated SQL, and run the agent under a separate, restricted database role Pedro et al., 2025. The controls that hold are the ones the model cannot touch.

Simon Willison compressed the structure into a rule of three: an agent with access to private data, exposure to untrusted content, and a way to communicate externally can be made to exfiltrate — no software bug required Simon Willison, 2025. His prescription is architectural: remove a leg. OWASP's mitigation list converges on the same ground — constrain what the model can do, and validate what it produces with deterministic code, because the model cannot be trusted to police itself.

Read those together and they say something stronger than "add defenses." They say: stop adjudicating strings. Change the interface.

No syntax to inject into

SQL injection didn't end because sanitizers got better. It ended because parameterized queries removed the place where data could become code. The same move is available to agents: refuse to let the model author executable syntax at all.

That is how SQAI is built. The model never writes SQL — not sanitized, not reviewed, none. Its one executing tool accepts a typed spec: a discriminated union, kind: "query" | "computation", version: "1", validated against a hash-pinned capability contract before anything runs. Name a capability outside the contract and the engine answers unsupported_operation. No string the model produces is ever concatenated, interpolated, or handed to an interpreter — so the thing an injection must seize, model-authored syntax, does not exist in the pipeline. The full argument is laid out in why agents shouldn't write SQL.

The vocabulary is closed, and it is read-only: every one of the 4,574 exposed capabilities, by construction. There is no write capability to name, so there is no incantation — however artfully smuggled into a support ticket — that produces a write. The host-code escape hatch, getUnsafeRuntime, is not reachable from any model tool.

And because serious security prices in the day the model is hijacked, the layers behind the interface assume it:

  • Policy the model cannot address. Allow-lists for sources, fields, and functions are fixed in your code at createSQAI() time and enforced in-process, before execution. The tool schema carries no allowed* fields — the policy surface never enters the context window. Violations return typed, non-retryable denials: policy_denied_source, policy_denied_field, policy_denied_function. Why this beats roles and flags: read-only that wasn't.
  • Results scoped to the tenant. Large results park under a result_id of 16 random bytes. A wrong tenant gets result_not_found — not a permissions error confirming the data exists — and entries expire in 15 minutes.
  • A bounded channel back to the model. At most 25 rows and 32,000 bytes return to the context window, truncation always declared. An injected prompt cannot make the tool dump a table into the transcript; the transport will not carry it.
  • Errors that don't draw a map. Model-visible errors pass through sanitizeMessage, which strips absolute filesystem paths. Reconnaissance gets a clean denial, not a directory listing.

Score it against the trifecta. Private data: still present — that is the job. Untrusted content: still present — models read what they read. But the legs that turn influence into loss are cut. The write path is absent, and the read path is allow-listed, tenant-scoped, capped — and hashed, so whatever a hijacked agent did manage to ask is replayable for the postmortem.

204 excluded 4,778 capabilities in the contract 4,574 exposed to the model — every one read-only 4,564 deterministic within the declared scope 0 write capabilities — nothing to name, nothing to run
The whole vocabulary, to scale — a hijacked prompt can pick different values, never a different verb (source: SQAI capability contract).

The boundary, redrawn

Prompt injection is not solved, and the record argues it will not be soon. LLM01 has held its rank for two editions running. A production system with Microsoft's defenses behind it lost to one email. Models will keep reading hostile text, and some of it will keep landing.

But influencing the model and reaching your database are different failures, and only the first is unavoidable. The Supabase ticket became an incident because the agent held service_role and a free-form SQL surface — precisely the legs a hijack needs. The same ticket, aimed at a closed, typed, read-only vocabulary, becomes a log line: unsupported_operation.

SQL injection's fix was never a smarter filter. It was a boundary where data cannot become code. Draw that boundary again — this time around the model — and the new SQL injection gets the old ending. The rest of the surface is documented on /security.