Bypassing Read-Only SQL Validation in DBHub
DBHub is Bytebase’s MCP server. It sits between an LLM agent and a Postgres, MySQL or SQL Server instance, and it is the component that decides which statements reach the database. Read-only mode is the flag you enable when the agent should look but not touch.
On the MySQL and MariaDB connectors, advisory GHSA-j656-3hf2-fvjc says that flag can be walked past, up to and including DROP TABLE. It covers @bytebase/dbhub below 0.22.6 and carries a medium severity. No CVE has been assigned, which is itself worth noticing: a lot of work happens at this severity level and rarely reaches a feed anyone monitors.
The bug is a two-character disagreement
src/utils/sql-parser.ts scans for -- and treats whatever follows as a comment, regardless of what that character is. MySQL and MariaDB are stricter: -- only opens a comment when the next character is whitespace, a control character, or the end of the line. Otherwise the two dashes are read as two minus signs and the rest of the line stays ordinary SQL.
So the two systems disagree about where the statement ends. The validator, believing it has found a comment, inspects a truncated string. The connector then executes the original.
That second half is the part people miss. src/tools/execute-sql.ts splits the input into statements and validates each one with isReadOnlySQL. But the MySQL and MariaDB connectors open their pool with multipleStatements: true and hand the unmodified string to the driver in a single call. Validation happened on a derived string. Execution happened on the real one. Anything that survives the split, or hides inside it, never gets checked.
What that means in practice
The validator inspects one thing, the database runs another, and read-only mode becomes advisory rather than enforced. The advisory’s example is a write or DDL statement reached through the MySQL connector; the shape of the bug is that the gateway’s opinion is not the one that executes.
I have not reproduced it against a specific release, so treat the mechanism as the advisory’s claim rather than my test result. The version range is the authority.
Reproducing it safely
You need a scratch database and a pinned DBHub version. Do not do this against anything you care about, and do not point it at a shared instance even in read-only mode, because the whole premise is that read-only mode may not hold.
docker run -d --rm -e MARIADB_ROOT_PASSWORD=dev -p 3306:3306 mariadb:11
Create a table with a row you can afford to lose, install @bytebase/dbhub@0.22.5, point it at that database with read-only mode enabled, and send a two-statement string where the first statement ends with -- immediately followed by a non-whitespace character. The specific bytes are in the advisory; the point of the exercise is to see whether your deployment behaves the way the advisory describes.
If the write lands, you have your answer. If it does not, you are on a fixed version or a driver that handles the input differently.
Do not fix this by parsing harder
The instinct is to add another regex, another dialect check, another normaliser. That is a denylist, and denylists lose: there is always another comment form, another quoting rule, another way to spell a statement the parser has not seen. This advisory is one instance of the category.
Enforce at the engine. Give the agent a database role that holds only SELECT on the schemas it needs, and connect with that role. MySQL and MariaDB will refuse the write regardless of what DBHub’s parser concluded, because the grant is checked by the server. Add a statement_timeout while you are there, and a MAX_EXECUTION_TIME hint if you run MySQL.
Two more things before shipping. Use a separate connection and a separate role per mode rather than a flag on a shared pool, so a configuration mistake cannot silently upgrade an agent’s privileges. And point read-only traffic at a replica when you have one, because the cheapest protection against a bad query is not being on the primary.
When not to do this
A read-only role does not save you from expensive queries. A SELECT with a careless join can still saturate a primary, and no grant prevents that. Timeouts and a replica are the answer there, not permissions.
And if your agent genuinely needs to write, none of this applies. You need a different design: a staging table, an approval step, an audit trail, and a human who can say no. A stricter parser is not a substitute for any of those, and this advisory is a fairly clean demonstration of why.