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 withWITH, notSELECT. Legitimate read-only queries fail silently. - False acceptances: PostgreSQL writable CTEs (
WITH deleted AS (DELETE FROM ...) SELECT ...) start withWITHbut mutate data. If the guard allowsWITHto fix the above, it opens a write path. - MySQL
SELECT INTO OUTFILEpasses the prefix check in haxzie/sequel-mcp — confirmed, not theoretical. Starts withSELECT, writes to the filesystem. - Multi-query chaining:
SELECT 1; DROP TABLE userspasses 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
- 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.
- Use parameterized queries — neither executeautomation nor haxzie/sequel-mcp separates parameters from query strings. Parameterized queries eliminate the SQL injection class entirely.
- 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.
- Do not trust the Anthropic reference server as a secure starting point. Issue #3314 is open. Audit
describe_tableand any PRAGMA-using path before deploying. - Test schema discovery tools against your actual database — MySQL 8.0 RDS returns 748 null entries from
list_tablesin executeautomation (issue #23, open). Agents silently fall back to rawinformation_schemaqueries, bypassing the tool abstraction.
Environments tested
| Tool | Version | Result |
|---|---|---|
| executeautomation/mcp-database-server | latest (Feb 2026) | source-reviewed: startsWith('select') prefix check found in source |
| modelcontextprotocol/servers mcp-server-sqlite | v2025.4.25 | source-reviewed: PRAGMA f-string interpolation found in source |
| haxzie/sequel-mcp | latest (Feb 2026) | source-reviewed: SELECT INTO OUTFILE passes prefix check |
| vanna-ai/vanna | v0.7.x | docs-reviewed: NL2SQL pipeline reviewed |
| Canner/WrenAI | latest (Feb 2026) | docs-reviewed: NL2SQL pipeline reviewed |
| Snowflake-Labs/ReFoRCE | latest (Feb 2026) | docs-reviewed: benchmark reviewed |
| xlang-ai/Spider2 | latest (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 |