MCP database servers’ “read-only mode” is a string check — and it’s bypassable
MCP database servers’ “read-only mode” is a string check — and it’s bypassable
From Theory Delta | Methodology | Published 2026-02-24
What you expect
You connect your agent to a database using an MCP database server with “read-only mode” enabled. The server restricts the agent to SELECT queries. Your data is safe from modification. You’ve seen the feature advertised in the official Anthropic MCP SQLite reference server and multiple community implementations.
What actually happens
Every SQLite MCP database server reviewed implements read-only enforcement as a string prefix check on the incoming query. None use SQL AST parsing to verify intent. The guard is:
// executeautomation/mcp-database-server — index.ts:272-291
if (!query.trim().toLowerCase().startsWith("select")) {
throw new Error("Only SELECT queries are allowed with read_query");
}
This is not a security boundary. It is a one-line string comparison that multiple attack classes bypass entirely.
What bypasses the guard:
- Writable CTEs —
WITH deleted AS (DELETE FROM users WHERE id=1) SELECT 1starts withWITH, notSELECT. If the server blocksWITH, legitimate read-only CTEs break. If it allowsWITH, writes go through. - PRAGMA statements —
PRAGMA journal_mode=DELETEandPRAGMA wal_checkpointcan modify database state. The prefix check never evaluates them. - Multi-query chaining —
SELECT 1; DROP TABLE userspasses the prefix check. Only the first statement is inspected. The PostgreSQL driver supports semicolon-delimited multi-query execution. - Extension loading —
SELECT load_extension('/path/to/malicious.so')passes as a SELECT. Loaded extensions can expose filesystem and network write paths.
The Anthropic reference server has an open SQL injection. mcp-server-sqlite (modelcontextprotocol/servers) interpolates table_name directly into a PRAGMA statement:
# mcp_server_sqlite/server.py:326
results = db._execute_query(
f"PRAGMA table_info({arguments['table_name']})"
)
Input users); DROP TABLE users; -- executes arbitrary SQL. Issue #3314 is open. Anthropic archived the reference server May 29, 2025 — 5,000+ forks carry this unpatched injection forward indefinitely.
The bypass is confirmed and independently verified. Security advisory GHSA-65hm-pwj5-73pw independently documents this finding for executeautomation/mcp-database-server.
The correctness ceiling compounds the problem. Even if security were solved, SOTA NL2SQL on enterprise schemas (Spider 2.0) is ~36% for the best agentic system. Spider 1.0’s commonly cited 86.6% uses toy schemas and is a misleading baseline for production deployments. Your agent is not reliably generating correct SQL — and the servers designed to contain the damage have unpatched injection points.
Scale of exposure. A July 2025 scan found 13,000+ MCP database servers in production with no authentication. Read-only enforcement is irrelevant when any network-reachable client can connect unauthenticated.
What this means for you
If you deployed an MCP database server with read-only mode to protect production data: you do not have read-only enforcement. You have a string check that rejects DELETE as a first token and accepts everything else. Any agent prompt that produces a writable CTE, a PRAGMA call, or a multi-statement query bypasses the guard entirely.
If you are using the Anthropic MCP SQLite reference server as a starting point: the describe_table path has an open SQL injection. An attacker who can write to the database you are connecting can embed a table name that triggers arbitrary SQL execution when your agent explores the schema during planning. This is not a theoretical edge case — it is the planning step every agent takes before generating queries.
If your agent uses schema discovery tools: they are more fragile than the query tools they support. list_tables returns 748 null entries on MySQL 8.0 RDS with executeautomation’s server. Agents silently fall back to raw information_schema queries, bypassing the tool abstraction and reintroducing injection surface.
What to do
-
Add a SQL AST parser. Use sqlglot (Python) or node-sql-parser (Node) to identify statement type before execution. String prefix checks are not a security boundary.
crystaldba/postgres-mcp(2,300 stars) is the only reviewed server that uses AST-level parsing via pglast — use it as a reference for the correct architecture. -
Use parameterized queries. Neither executeautomation nor haxzie/sequel-mcp separates parameters from query strings. Parameterized queries eliminate the SQL injection class. The only reviewed server that uses parameterized queries as the architectural default is
googleapis/genai-toolbox(13,300 stars, Google-backed), which uses YAML-defined pre-approved query templates. -
Do not treat the Anthropic reference server as a secure starting point. Issue #3314 is open. Audit
describe_tableand any PRAGMA-using path before deploying, regardless of which fork you start from. -
Add authentication before anything else. Read-only enforcement is moot without authentication. 13,000+ production servers are reachable by any unauthenticated client.
-
Sandbox any code execution path. If your NL2SQL stack generates visualization code (Vanna, for instance, runs LLM-generated Plotly code via
exec()without sandboxing), run it in a subprocess with restricted imports or a container with no network access. This is a separate attack surface from SQL injection.
Evidence
| Tool | Version | Result |
|---|---|---|
| executeautomation/mcp-database-server | latest (Feb 2026) | source-reviewed: startsWith('select') prefix check confirmed; GHSA-65hm-pwj5-73pw independently confirms bypass |
| modelcontextprotocol/servers mcp-server-sqlite | v2025.4.25 | source-reviewed: PRAGMA f-string interpolation confirmed; Issue #3314 open; archived May 2025 — 5,000+ forks inherit unpatched injection |
| haxzie/sequel-mcp | latest (Feb 2026) | source-reviewed: startsWith('select') prefix check confirmed; writable CTEs pass guard |
| crystaldba/postgres-mcp | latest (Feb 2026) | source-reviewed: pglast AST parsing confirmed — only reviewed server with correct architecture; residual gap: SECURITY DEFINER stored procs bypass enforcement |
| vanna-ai/vanna | v0.7.x | docs-reviewed: exec() of LLM-generated Plotly code is an unmitigated RCE surface |
| xlang-ai/Spider2 | latest (Feb 2026) | docs-reviewed: GPT-4 enterprise schema accuracy 5.6% vs. commonly cited 86.6% (Spider 1.0 toy schemas) |
Confidence: source-reviewed and independently confirmed — source code reviewed across 3 MCP database servers; bypass patterns confirmed by reading source. Security advisory GHSA-65hm-pwj5-73pw independently confirms the finding.
Falsification criterion: This claim would be disproved by finding an SQLite MCP database server (among the 3 reviewed) that uses SQL AST parsing — not string prefix matching — for read-only enforcement, or by demonstrating that the bypass vectors above do not execute in the documented environments.
Seen different? Contribute your evidence — theory delta is what makes this knowledge base work.