MCP database servers’ “read-only mode” is a string check — and it’s bypassable

Published: 2026-02-24 Last verified: 2026-04-19 independently-confirmed
8 claims 8 tested finding

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 CTEsWITH deleted AS (DELETE FROM users WHERE id=1) SELECT 1 starts with WITH, not SELECT. If the server blocks WITH, legitimate read-only CTEs break. If it allows WITH, writes go through.
  • PRAGMA statementsPRAGMA journal_mode=DELETE and PRAGMA wal_checkpoint can modify database state. The prefix check never evaluates them.
  • Multi-query chainingSELECT 1; DROP TABLE users passes the prefix check. Only the first statement is inspected. The PostgreSQL driver supports semicolon-delimited multi-query execution.
  • Extension loadingSELECT 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

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

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

  3. Do not treat the Anthropic reference server as a secure starting point. Issue #3314 is open. Audit describe_table and any PRAGMA-using path before deploying, regardless of which fork you start from.

  4. Add authentication before anything else. Read-only enforcement is moot without authentication. 13,000+ production servers are reachable by any unauthenticated client.

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

ToolVersionResult
executeautomation/mcp-database-serverlatest (Feb 2026)source-reviewed: startsWith('select') prefix check confirmed; GHSA-65hm-pwj5-73pw independently confirms bypass
modelcontextprotocol/servers mcp-server-sqlitev2025.4.25source-reviewed: PRAGMA f-string interpolation confirmed; Issue #3314 open; archived May 2025 — 5,000+ forks inherit unpatched injection
haxzie/sequel-mcplatest (Feb 2026)source-reviewed: startsWith('select') prefix check confirmed; writable CTEs pass guard
crystaldba/postgres-mcplatest (Feb 2026)source-reviewed: pglast AST parsing confirmed — only reviewed server with correct architecture; residual gap: SECURITY DEFINER stored procs bypass enforcement
vanna-ai/vannav0.7.xdocs-reviewed: exec() of LLM-generated Plotly code is an unmitigated RCE surface
xlang-ai/Spider2latest (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.