Published: 2026-02-24 Last verified: 2026-02-22 independently-confirmed
8 claims 0 tested finding

Every MCP database server enforces read-only with a string check — and it’s bypassable

From Theory Delta | Methodology | Published 2026-02-24

What the docs say

MCP database servers advertise a “read-only mode” that restricts agents to SELECT queries, preventing data modification. The official Anthropic MCP SQLite reference server and community implementations all offer this as a safety feature for connecting agents to production databases.

What actually happens

Every MCP database server reviewed implements read-only enforcement as a string prefix check. None use SQL AST parsing. Here is the actual code:

executeautomation/mcp-database-server (source, security advisory GHSA-65hm-pwj5-73pw):

// index.ts:272-291
if (!query.trim().toLowerCase().startsWith("select")) {
  throw new Error("Only SELECT queries are allowed with read_query");
}

modelcontextprotocol/servers mcp-server-sqlite (issue #3314):

# mcp_server_sqlite/server.py:326
results = db._execute_query(
    f"PRAGMA table_info({arguments['table_name']})"
)

The table_name parameter is interpolated directly into the PRAGMA statement without sanitization. Input like users); DROP TABLE users; -- executes arbitrary SQL. The reference implementation that builders copy from has an open SQL injection.

This means:

  • False rejections: CTEs (WITH ... AS (...) SELECT ...) are blocked because they start with WITH, not SELECT. Legitimate read-only queries fail silently.
  • False acceptances: PostgreSQL writable CTEs (WITH deleted AS (DELETE FROM ...) SELECT ...) start with WITH but mutate data. If the guard allows WITH to fix the above, it opens a write path.
  • MySQL SELECT INTO OUTFILE passes the prefix check in haxzie/sequel-mcp — confirmed, not theoretical. Starts with SELECT, writes to the filesystem.
  • Multi-query chaining: SELECT 1; DROP TABLE users passes the prefix check because only the first statement is inspected. The pg driver supports multi-query execution when statements are semicolon-terminated.

The NL2SQL correctness ceiling compounds the problem: SOTA on enterprise schemas (Spider 2.0) is ~17% for o1-preview, ~10% for GPT-4o. Spider 1.0’s commonly cited 86.6% uses toy schemas and is a misleading baseline.

What to do instead

  1. Add a SQL AST parser (sqlglot for Python, node-sql-parser for Node) that identifies statement type before execution. String prefix checks are not a security boundary.
  2. Use parameterized queries — neither executeautomation nor haxzie/sequel-mcp separates parameters from query strings. Parameterized queries eliminate the SQL injection class entirely.
  3. Sandbox any code execution path — if your NL2SQL stack generates visualization code or transformation scripts, run them in a subprocess with restricted imports or a container with no network access. Input filtering will not stop LLM role-switching attacks.
  4. Do not trust the Anthropic reference server as a secure starting point. Issue #3314 is open. Audit describe_table and any PRAGMA-using path before deploying.
  5. Test schema discovery tools against your actual database — MySQL 8.0 RDS returns 748 null entries from list_tables in executeautomation (issue #23, open). Agents silently fall back to raw information_schema queries, bypassing the tool abstraction.

Environments tested

ToolVersionResult
executeautomation/mcp-database-serverlatest (Feb 2026)source-reviewed: startsWith('select') prefix check found in source
modelcontextprotocol/servers mcp-server-sqlitev2025.4.25source-reviewed: PRAGMA f-string interpolation found in source
haxzie/sequel-mcplatest (Feb 2026)source-reviewed: SELECT INTO OUTFILE passes prefix check
vanna-ai/vannav0.7.xdocs-reviewed: NL2SQL pipeline reviewed
Canner/WrenAIlatest (Feb 2026)docs-reviewed: NL2SQL pipeline reviewed
Snowflake-Labs/ReFoRCElatest (Feb 2026)docs-reviewed: benchmark reviewed
xlang-ai/Spider2latest (Feb 2026)docs-reviewed: enterprise accuracy benchmarks reviewed

Confidence and gaps

Confidence: source-reviewed — source code reviewed across 3 MCP database servers (executeautomation, mcp-server-sqlite, sequel-mcp), benchmarks reviewed across 4 additional tools. No commands were executed against running instances; bypass patterns were confirmed by reading source code. Security advisory GHSA-65hm-pwj5-73pw independently confirms the finding.

Falsification criterion: This claim would be disproved by finding an 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 startsWith('select') check cannot be bypassed via CTEs, PRAGMA injection, or SELECT INTO OUTFILE in the documented environments.

Open questions: Does any MCP database server implement AST-based read-only enforcement? Are there production deployments using parameterized queries through MCP tool calls? Has anyone shipped a sandboxed execution path for NL2SQL visualization code?

Seen different? Contribute your evidence — theory delta is what makes this knowledge base work.

Environments Tested

Tool Version Result
executeautomation/mcp-database-server latest (Feb 2026) startsWith('select') prefix check found in source
modelcontextprotocol/servers mcp-server-sqlite v2025.4.25 PRAGMA f-string interpolation found in source
haxzie/sequel-mcp latest (Feb 2026) SELECT INTO OUTFILE passes prefix check
vanna-ai/vanna v0.7.x NL2SQL pipeline reviewed
Canner/WrenAI latest (Feb 2026) NL2SQL pipeline reviewed
Snowflake-Labs/ReFoRCE latest (Feb 2026) Benchmark reviewed
xlang-ai/Spider2 latest (Feb 2026) Enterprise accuracy benchmarks reviewed