Post

CVE-2026-48774: MCP run_sql_readonly multi-statement bypass in ProxySQL

CVE-2026-48774: MCP run_sql_readonly multi-statement bypass in ProxySQL

I reported a GenAI/MCP read-only policy bypass in ProxySQL that was published as CVE-2026-48774 / GHSA-7wh6-2vcc-gcm4.

The issue affected ProxySQL’s MCP run_sql_readonly tool for MySQL targets. The tool was documented and exposed as a read-only SQL helper, but it validated the full input string with a weak blacklist and then executed the original SQL string on a MySQL connection created with CLIENT_MULTI_STATEMENTS.

That allowed an MCP caller to place a read-only statement first and a side-effecting statement second.

Advisory

  • CVE: CVE-2026-48774
  • GitHub Advisory: GHSA-7wh6-2vcc-gcm4
  • Package: proxysql
  • Affected versions: >= 4.0.6, <= 4.0.8
  • Patched version: 4.0.9
  • Severity: High 7.5/10
  • CVSS: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N
  • CWE: CWE-20: Improper Input Validation

Root Cause

The vulnerable MCP tool had two conflicting properties:

  1. run_sql_readonly was intended to enforce a read-only SQL contract.
  2. The MySQL backend connection allowed multi-statement execution.

The validator did not parse SQL per statement. It uppercased the complete input, rejected a fixed list of dangerous substrings, and accepted the query if it started with an allowed keyword such as SELECT.

That created a bypass shape like:

1
SELECT 1; RENAME TABLE testdb.mcp_side_effect TO testdb.mcp_side_effect_renamed

The first statement satisfied the read-only-looking prefix check. The second statement was still sent to MySQL because the backend connection was created with CLIENT_MULTI_STATEMENTS.

The handler then stored only the first result and returned the connection to the pool without draining all multi-statement results, which also produced a secondary Commands out of sync reliability symptom.

Attack Shape

The practical attack requires MCP to be built and enabled, and the attacker must be able to reach /mcp/query.

The strongest unauthenticated deployment shape is:

  1. MCP is enabled.
  2. /mcp/query is reachable from an attacker-controlled network location.
  3. mcp-query_endpoint_auth is empty.
  4. A MySQL MCP target is configured with credentials that can modify database state.

The demonstrated payload used run_sql_readonly with a first SELECT statement followed by RENAME TABLE.

ProxySQL returned a successful result for the first SELECT, while direct backend verification showed that the table had actually been renamed.

Validation Notes

The initial runtime proof used a live MCP setup with a MariaDB backend. The important observation was that the MCP response only reported the first SELECT result, while backend verification showed the table had been renamed.

I then repeated the same backend-write test against official ProxySQL AI/MCP Tier release packages:

  • v4.0.6;
  • v4.0.7;
  • v4.0.8.

In each case, the original table disappeared, the renamed table existed, and a row count on the renamed table still returned 1. The next MCP query returned Commands out of sync, which was a useful secondary signal that the handler had executed a multi-statement payload and returned a connection with unread results to the pool.

Static mirror validation also helped separate the bug from a single RENAME TABLE trick. Other side-effecting MySQL statements such as SET, RESET, LOCK TABLES, and KILL were not covered by the blacklist shape used by the read-only validator.

Why This Was Subtle

This was not simply “a SQL tool can run SQL.”

The security boundary was the documented read-only contract of the MCP tool. A caller authorized to use run_sql_readonly was not supposed to get arbitrary write or administrative SQL execution through that tool.

The bug appeared because several individually understandable choices were combined:

  • a first-keyword allowlist;
  • a substring blacklist instead of a parser;
  • MySQL multi-statements enabled on the backend connection;
  • execution of the original un-split SQL string;
  • no per-statement read-only authorization before execution.

The result was a policy bypass before the database privilege boundary. The final impact still depends on the configured MCP target account, but the MCP-level read-only promise was already broken.

Impact

An attacker who can call /mcp/query can execute side-effecting MySQL statements under the configured MCP backend target credentials.

The demonstrated impact was backend integrity compromise: a supposedly read-only MCP request renamed a table on the backend database.

Depending on the target account privileges, the same primitive can allow:

  • schema or data modification;
  • administrative SQL execution;
  • operational disruption through statements such as locks, kills, resets, or other side-effecting commands;
  • connection-pool reliability issues from unread multi-statement results.

The exploit is limited by the backend credentials configured for the MCP target. That limitation does not remove the vulnerability, because the caller was authorized for the documented read-only tool, not for arbitrary write/admin SQL.

Fix Direction

The fix should enforce read-only semantics before execution and avoid multi-statement ambiguity.

Defensive principles:

  • do not use CLIENT_MULTI_STATEMENTS for run_sql_readonly backend connections;
  • reject multi-statement input before execution;
  • replace substring blacklist validation with parser-backed SQL classification;
  • authorize every parsed statement, not only the first token of the full input;
  • treat semicolons outside string literals and comments as invalid unless real SQL parsing is used;
  • use database-level read-only credentials for MCP read-only targets;
  • if multi-statements are ever intentionally supported, drain all results before returning a connection to the pool.

References

This post is licensed under CC BY 4.0 by the author.