fix(tool/mysql-get-query-plan): prevent query execution bypass and statement injection#3235
Conversation
EXPLAIN FORMAT=JSON ANALYZE <stmt> causes MySQL 8.0+ to actually execute the statement rather than just plan it, bypassing the read-only intent of the tool. Any caller who can supply the sql_statement parameter could run arbitrary queries (SELECT on privileged tables, SLEEP-based DoS, etc.) or inject additional statements via semicolons. Add ValidateSQLStatement to allowlist only the DML prefixes that MySQL EXPLAIN legitimately supports (SELECT, INSERT, UPDATE, DELETE, REPLACE, TABLE, WITH, VALUES, FOR) and reject semicolons. Add unit tests covering both the happy path and all attack variants.
There was a problem hiding this comment.
Code Review
This pull request introduces a SQL validation layer to the MySQL query plan tool, aimed at preventing multi-statement injection and accidental query execution via the ANALYZE keyword. While the addition of validation and unit tests is a positive security improvement, the current implementation has several issues. Specifically, the FOR keyword should be removed from the allowlist as it is incompatible with the tool's hardcoded JSON output format. Additionally, the semicolon check is too broad and will trigger false positives for valid queries containing semicolons in string literals. Finally, the use of strings.Fields for tokenization is fragile, as it fails to account for valid SQL starting with comments or parentheses.
|
Hi @evilgensec, thanks for opening the PR! |
- Remove FOR from the allowlist (EXPLAIN FOR CONNECTION does not support FORMAT=JSON) and from the error message. - Replace strings.Fields/raw semicolon check with a string- and comment-aware scanner so leading parentheses (parenthesized UNION), leading comments, a single trailing semicolon, and semicolons inside string literals are accepted; only true multi-statement input is rejected. - Document the validation rules in the tool docs and in the mysql / cloud-sql-mysql prebuilt tool descriptions. - Extend unit tests for the new accepted/rejected cases.
|
@duwenxin99 Can you please review the changes |
averikitsch
left a comment
There was a problem hiding this comment.
The current approach of manually scanning and validating SQL statements to prevent injection and unintended execution (like EXPLAIN ANALYZE) has several drawbacks:
- Maintenance and Complexity (Too Restrictive): Maintaining a custom parser to stay in sync with the MySQL dialect is challenging. Valid but complex syntax, such as MySQL-specific executable comments (
/*! ... */) or rare DML forms, might be incorrectly rejected. This high rate of false positives can hinder legitimate use cases and create a maintenance burden as the database engine evolves. - Security Gaps (Incomplete Coverage): Hand-written scanners are prone to bypasses. For instance, subtle variations in character encoding, nested comments, or specific SQL hints might evade the
scanStatementlogic , potentially allowing for statement injection or unauthorized query execution that the validator is designed to prevent. Relying on manual logic to secure a database interface is often less robust than using a formal grammar or a limited-privilege database user.
…of hand-written SQL validation Address review feedback that a manual SQL scanner is the wrong mechanism: it is both too restrictive (false positives on executable comments, parenthesized UNION, rare DML) and bypassable (encoding, nested comments, hints). Verified against MySQL 8.4 and 9.7 through the Go driver that the threats the scanner targeted are already neutralized structurally: - Plain EXPLAIN (without ANALYZE) never executes the wrapped statement (SELECT/INSERT/UPDATE/DELETE all return a plan with no side effects). - EXPLAIN ANALYZE is unreachable: the tool fixes the FORMAT=JSON prefix and MySQL requires ANALYZE to precede FORMAT=, so an injected ANALYZE is a server-side syntax error. - Multi-statement input is rejected by the driver (multiStatements is off by default), so "SELECT 1; DROP TABLE t" does not execute. Remove explainableStatements/ValidateSQLStatement/scanStatement and their tests. Document the real security model (EXPLAIN does not execute; configure the source with a least-privilege database user) in the tool docs and prebuilt descriptions. Keep the evalueate -> evaluate typo fix.
|
@averikitsch @duwenxin99 thanks for the review. I agree with the concern that a hand-written SQL scanner is the wrong mechanism — it is both too restrictive (false positives on
Given that, and that the sibling tools ( Net change is −125 LOC. Build, |
|
Hi Team, anything else needed to merge? |
|
/gcbrun |
|
/gcbrun |
…s and statement injection (#3235) ## Summary `mysql-get-query-plan` builds its EXPLAIN query by directly interpolating the caller-supplied `sql_statement` into `EXPLAIN FORMAT=JSON %s` with no validation: ```go // internal/tools/mysql/mysqlgetqueryplan/mysqlgetqueryplan.go:118 query := fmt.Sprintf("EXPLAIN FORMAT=JSON %s", sqlStr) result, err := source.RunSQL(ctx, query, nil) ``` ### Attack 1 — EXPLAIN ANALYZE execution bypass MySQL 8.0+ treats `EXPLAIN [FORMAT=JSON] ANALYZE <stmt>` as an **execution** primitive: the statement is actually run (with full I/O) and the measured runtime statistics are returned. Supplying ``` sql_statement = "ANALYZE SELECT password FROM mysql.user WHERE user='root'" ``` causes the tool to issue ```sql EXPLAIN FORMAT=JSON ANALYZE SELECT password FROM mysql.user WHERE user='root' ``` which MySQL executes. The `actual_rows` field in the returned JSON is non-zero when the WHERE condition matches, enabling boolean-based blind extraction of arbitrary data. Heavier variants (long `SLEEP`, table scans on huge tables) yield denial-of-service. ### Attack 2 — multi-statement injection via semicolons Without semicolon filtering, a caller can append additional statements: ``` sql_statement = "SELECT 1; DROP TABLE orders" ``` Many MySQL drivers execute each semicolon-separated statement in sequence. ## Fix `ValidateSQLStatement` (new, exported for testing): 1. Rejects any input containing `;`. 2. Allowlists only the DML prefixes MySQL's EXPLAIN legitimately supports: `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `REPLACE`, `TABLE`, `WITH`, `VALUES`, `FOR`. This excludes `ANALYZE`, `DROP`, `CREATE`, `CALL`, and all other non-explainable forms. 13 unit tests added (happy-path DML variants + all attack variants). ## Test plan - [x] `go build ./internal/tools/mysql/mysqlgetqueryplan/...` — clean - [x] `go test ./internal/tools/mysql/mysqlgetqueryplan/... -run TestValidateSQLStatement -v` — 13/13 PASS - [x] `ANALYZE` payload → rejected at input validation, never reaches `RunSQL` - [x] Semicolon payload → rejected at input validation - [x] Normal `SELECT` queries → pass through unchanged --------- Co-authored-by: evilgensec <[email protected]> Co-authored-by: Averi Kitsch <[email protected]> 7ed1e7b
…s and statement injection (googleapis#3235) ## Summary `mysql-get-query-plan` builds its EXPLAIN query by directly interpolating the caller-supplied `sql_statement` into `EXPLAIN FORMAT=JSON %s` with no validation: ```go // internal/tools/mysql/mysqlgetqueryplan/mysqlgetqueryplan.go:118 query := fmt.Sprintf("EXPLAIN FORMAT=JSON %s", sqlStr) result, err := source.RunSQL(ctx, query, nil) ``` ### Attack 1 — EXPLAIN ANALYZE execution bypass MySQL 8.0+ treats `EXPLAIN [FORMAT=JSON] ANALYZE <stmt>` as an **execution** primitive: the statement is actually run (with full I/O) and the measured runtime statistics are returned. Supplying ``` sql_statement = "ANALYZE SELECT password FROM mysql.user WHERE user='root'" ``` causes the tool to issue ```sql EXPLAIN FORMAT=JSON ANALYZE SELECT password FROM mysql.user WHERE user='root' ``` which MySQL executes. The `actual_rows` field in the returned JSON is non-zero when the WHERE condition matches, enabling boolean-based blind extraction of arbitrary data. Heavier variants (long `SLEEP`, table scans on huge tables) yield denial-of-service. ### Attack 2 — multi-statement injection via semicolons Without semicolon filtering, a caller can append additional statements: ``` sql_statement = "SELECT 1; DROP TABLE orders" ``` Many MySQL drivers execute each semicolon-separated statement in sequence. ## Fix `ValidateSQLStatement` (new, exported for testing): 1. Rejects any input containing `;`. 2. Allowlists only the DML prefixes MySQL's EXPLAIN legitimately supports: `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `REPLACE`, `TABLE`, `WITH`, `VALUES`, `FOR`. This excludes `ANALYZE`, `DROP`, `CREATE`, `CALL`, and all other non-explainable forms. 13 unit tests added (happy-path DML variants + all attack variants). ## Test plan - [x] `go build ./internal/tools/mysql/mysqlgetqueryplan/...` — clean - [x] `go test ./internal/tools/mysql/mysqlgetqueryplan/... -run TestValidateSQLStatement -v` — 13/13 PASS - [x] `ANALYZE` payload → rejected at input validation, never reaches `RunSQL` - [x] Semicolon payload → rejected at input validation - [x] Normal `SELECT` queries → pass through unchanged --------- Co-authored-by: evilgensec <[email protected]> Co-authored-by: Averi Kitsch <[email protected]> 7ed1e7b
🤖 I have created a release *beep* *boop* --- ## [1.5.0](v1.4.0...v1.5.0) (2026-06-18) ### Features * **auth/google:** Require audience or clientId for mcpEnabled ([#3450](#3450)) ([59f7b6e](59f7b6e)) * Enable per source level flags for sql commenter ([#3465](#3465)) ([ecce6b7](ecce6b7)) * **mcp:** Add URL parameter binding for HTTP transport ([#3112](#3112)) ([0cc7b37](0cc7b37)) * **scylladb:** Adding support for ScyllaDB source and tool ([#3119](#3119)) ([2dada83](2dada83)) * **server:** Add support for toolset filtering in prebuilt CLI flag ([#3245](#3245)) ([7cc4f65](7cc4f65)) * **skills:** Generate skills offline without live source connections ([#3388](#3388)) ([4c860b6](4c860b6)) * **skills:** Tolerate missing env vars during offline skills-generate ([#3399](#3399)) ([ea5d3e5](ea5d3e5)) * **source/cloud-storage:** Restrict bucket and local path access ([#3454](#3454)) ([2c3ca5d](2c3ca5d)) * **tools/bigquery:** Add per tool query label in BigQuery jobs ([#1975](#1975)) ([3f6a49f](3f6a49f)) * **tools/dataplex:** Add tools to support metadata enrichment workflow ([#3270](#3270)) ([05289aa](05289aa)) * **tools/mysql:** Add show-query-stats and list-all-locks tools for MySQL and Cloud SQL MySQL source ([#2954](#2954)) ([a9693bd](a9693bd)) * **tools:** Decouple tool initialization from sources ([#3355](#3355)) ([32a24e3](32a24e3)) ### Bug Fixes * **auth/dataplex:** Fix failing source with service account credentials ([#3369](#3369)) ([ba4deef](ba4deef)) * **bigquery:** Wire maximumBytesBilled into prebuilt config ([#3385](#3385)) ([4abbf6e](4abbf6e)) * Bound MCP HTTP body size ([#3216](#3216)) ([d4f4342](d4f4342)) * **config:** Add doc/line context to parse errors ([#2957](#2957)) ([4b097da](4b097da)) * Escape delimiter characters in applyEscape to prevent SQL injection ([#2811](#2811)) ([932519a](932519a)) * **npm:** Source binary version from cmd/version.txt ([#3417](#3417)) ([6ffbdec](6ffbdec)) * **prebuilt/alloydb-omni:** Require password env var explicitly ([#3398](#3398)) ([fcbe3e7](fcbe3e7)) * **server:** Fail if MCP auth is enabled together with enable-api ([#3435](#3435)) ([a6ff910](a6ff910)) * **server:** Return errors instead of panicking in InitializeConfigs ([#3397](#3397)) ([f48b01d](f48b01d)) * **source/cloudhealthcare:** Validate pageURL parameter to prevent SSRF ([#3453](#3453)) ([9abf47d](9abf47d)) * **source/dataplex,source/datalineage:** Specify cloud-platform scope for default credentials ([#3376](#3376)) ([13e8c36](13e8c36)) * **source/http:** Implement SSRF guard ([#3448](#3448)) ([24d7d29](24d7d29)) * **tool/bigquery-execute-sql:** Prevent dataset restriction bypass ([#3452](#3452)) ([ca6d5e3](ca6d5e3)) * **tool/mysql-get-query-plan:** Prevent query execution bypass and statement injection ([#3235](#3235)) ([7ed1e7b](7ed1e7b)) * **tool/spanner-sql,tool/spanner-execute-sql:** Use read-only annotations when readOnly is set ([#3338](#3338)) ([8bde0ec](8bde0ec)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --------- Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Yuan Teoh <[email protected]>
🤖 I have created a release *beep* *boop* --- ## [1.5.0](v1.4.0...v1.5.0) (2026-06-18) ### Features * **auth/google:** Require audience or clientId for mcpEnabled ([#3450](#3450)) ([59f7b6e](59f7b6e)) * Enable per source level flags for sql commenter ([#3465](#3465)) ([ecce6b7](ecce6b7)) * **mcp:** Add URL parameter binding for HTTP transport ([#3112](#3112)) ([0cc7b37](0cc7b37)) * **scylladb:** Adding support for ScyllaDB source and tool ([#3119](#3119)) ([2dada83](2dada83)) * **server:** Add support for toolset filtering in prebuilt CLI flag ([#3245](#3245)) ([7cc4f65](7cc4f65)) * **skills:** Generate skills offline without live source connections ([#3388](#3388)) ([4c860b6](4c860b6)) * **skills:** Tolerate missing env vars during offline skills-generate ([#3399](#3399)) ([ea5d3e5](ea5d3e5)) * **source/cloud-storage:** Restrict bucket and local path access ([#3454](#3454)) ([2c3ca5d](2c3ca5d)) * **tools/bigquery:** Add per tool query label in BigQuery jobs ([#1975](#1975)) ([3f6a49f](3f6a49f)) * **tools/dataplex:** Add tools to support metadata enrichment workflow ([#3270](#3270)) ([05289aa](05289aa)) * **tools/mysql:** Add show-query-stats and list-all-locks tools for MySQL and Cloud SQL MySQL source ([#2954](#2954)) ([a9693bd](a9693bd)) * **tools:** Decouple tool initialization from sources ([#3355](#3355)) ([32a24e3](32a24e3)) ### Bug Fixes * **auth/dataplex:** Fix failing source with service account credentials ([#3369](#3369)) ([ba4deef](ba4deef)) * **bigquery:** Wire maximumBytesBilled into prebuilt config ([#3385](#3385)) ([4abbf6e](4abbf6e)) * Bound MCP HTTP body size ([#3216](#3216)) ([d4f4342](d4f4342)) * **config:** Add doc/line context to parse errors ([#2957](#2957)) ([4b097da](4b097da)) * Escape delimiter characters in applyEscape to prevent SQL injection ([#2811](#2811)) ([932519a](932519a)) * **npm:** Source binary version from cmd/version.txt ([#3417](#3417)) ([6ffbdec](6ffbdec)) * **prebuilt/alloydb-omni:** Require password env var explicitly ([#3398](#3398)) ([fcbe3e7](fcbe3e7)) * **server:** Fail if MCP auth is enabled together with enable-api ([#3435](#3435)) ([a6ff910](a6ff910)) * **server:** Return errors instead of panicking in InitializeConfigs ([#3397](#3397)) ([f48b01d](f48b01d)) * **source/cloudhealthcare:** Validate pageURL parameter to prevent SSRF ([#3453](#3453)) ([9abf47d](9abf47d)) * **source/dataplex,source/datalineage:** Specify cloud-platform scope for default credentials ([#3376](#3376)) ([13e8c36](13e8c36)) * **source/http:** Implement SSRF guard ([#3448](#3448)) ([24d7d29](24d7d29)) * **tool/bigquery-execute-sql:** Prevent dataset restriction bypass ([#3452](#3452)) ([ca6d5e3](ca6d5e3)) * **tool/mysql-get-query-plan:** Prevent query execution bypass and statement injection ([#3235](#3235)) ([7ed1e7b](7ed1e7b)) * **tool/spanner-sql,tool/spanner-execute-sql:** Use read-only annotations when readOnly is set ([#3338](#3338)) ([8bde0ec](8bde0ec)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --------- Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Yuan Teoh <[email protected]> 45c0f86
🤖 I have created a release *beep* *boop* --- ## [1.5.0](googleapis/mcp-toolbox@v1.4.0...v1.5.0) (2026-06-18) ### Features * **auth/google:** Require audience or clientId for mcpEnabled ([googleapis#3450](googleapis#3450)) ([59f7b6e](googleapis@59f7b6e)) * Enable per source level flags for sql commenter ([googleapis#3465](googleapis#3465)) ([ecce6b7](googleapis@ecce6b7)) * **mcp:** Add URL parameter binding for HTTP transport ([googleapis#3112](googleapis#3112)) ([0cc7b37](googleapis@0cc7b37)) * **scylladb:** Adding support for ScyllaDB source and tool ([googleapis#3119](googleapis#3119)) ([2dada83](googleapis@2dada83)) * **server:** Add support for toolset filtering in prebuilt CLI flag ([googleapis#3245](googleapis#3245)) ([7cc4f65](googleapis@7cc4f65)) * **skills:** Generate skills offline without live source connections ([googleapis#3388](googleapis#3388)) ([4c860b6](googleapis@4c860b6)) * **skills:** Tolerate missing env vars during offline skills-generate ([googleapis#3399](googleapis#3399)) ([ea5d3e5](googleapis@ea5d3e5)) * **source/cloud-storage:** Restrict bucket and local path access ([googleapis#3454](googleapis#3454)) ([2c3ca5d](googleapis@2c3ca5d)) * **tools/bigquery:** Add per tool query label in BigQuery jobs ([googleapis#1975](googleapis#1975)) ([3f6a49f](googleapis@3f6a49f)) * **tools/dataplex:** Add tools to support metadata enrichment workflow ([googleapis#3270](googleapis#3270)) ([05289aa](googleapis@05289aa)) * **tools/mysql:** Add show-query-stats and list-all-locks tools for MySQL and Cloud SQL MySQL source ([googleapis#2954](googleapis#2954)) ([a9693bd](googleapis@a9693bd)) * **tools:** Decouple tool initialization from sources ([googleapis#3355](googleapis#3355)) ([32a24e3](googleapis@32a24e3)) ### Bug Fixes * **auth/dataplex:** Fix failing source with service account credentials ([googleapis#3369](googleapis#3369)) ([ba4deef](googleapis@ba4deef)) * **bigquery:** Wire maximumBytesBilled into prebuilt config ([googleapis#3385](googleapis#3385)) ([4abbf6e](googleapis@4abbf6e)) * Bound MCP HTTP body size ([googleapis#3216](googleapis#3216)) ([d4f4342](googleapis@d4f4342)) * **config:** Add doc/line context to parse errors ([googleapis#2957](googleapis#2957)) ([4b097da](googleapis@4b097da)) * Escape delimiter characters in applyEscape to prevent SQL injection ([googleapis#2811](googleapis#2811)) ([932519a](googleapis@932519a)) * **npm:** Source binary version from cmd/version.txt ([googleapis#3417](googleapis#3417)) ([6ffbdec](googleapis@6ffbdec)) * **prebuilt/alloydb-omni:** Require password env var explicitly ([googleapis#3398](googleapis#3398)) ([fcbe3e7](googleapis@fcbe3e7)) * **server:** Fail if MCP auth is enabled together with enable-api ([googleapis#3435](googleapis#3435)) ([a6ff910](googleapis@a6ff910)) * **server:** Return errors instead of panicking in InitializeConfigs ([googleapis#3397](googleapis#3397)) ([f48b01d](googleapis@f48b01d)) * **source/cloudhealthcare:** Validate pageURL parameter to prevent SSRF ([googleapis#3453](googleapis#3453)) ([9abf47d](googleapis@9abf47d)) * **source/dataplex,source/datalineage:** Specify cloud-platform scope for default credentials ([googleapis#3376](googleapis#3376)) ([13e8c36](googleapis@13e8c36)) * **source/http:** Implement SSRF guard ([googleapis#3448](googleapis#3448)) ([24d7d29](googleapis@24d7d29)) * **tool/bigquery-execute-sql:** Prevent dataset restriction bypass ([googleapis#3452](googleapis#3452)) ([ca6d5e3](googleapis@ca6d5e3)) * **tool/mysql-get-query-plan:** Prevent query execution bypass and statement injection ([googleapis#3235](googleapis#3235)) ([7ed1e7b](googleapis@7ed1e7b)) * **tool/spanner-sql,tool/spanner-execute-sql:** Use read-only annotations when readOnly is set ([googleapis#3338](googleapis#3338)) ([8bde0ec](googleapis@8bde0ec)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --------- Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Yuan Teoh <[email protected]> 45c0f86
🤖 I have created a release *beep* *boop* --- ## [1.5.0](googleapis/mcp-toolbox@v1.4.0...v1.5.0) (2026-06-18) ### Features * **auth/google:** Require audience or clientId for mcpEnabled ([googleapis#3450](googleapis#3450)) ([59f7b6e](googleapis@59f7b6e)) * Enable per source level flags for sql commenter ([googleapis#3465](googleapis#3465)) ([ecce6b7](googleapis@ecce6b7)) * **mcp:** Add URL parameter binding for HTTP transport ([googleapis#3112](googleapis#3112)) ([0cc7b37](googleapis@0cc7b37)) * **scylladb:** Adding support for ScyllaDB source and tool ([googleapis#3119](googleapis#3119)) ([2dada83](googleapis@2dada83)) * **server:** Add support for toolset filtering in prebuilt CLI flag ([googleapis#3245](googleapis#3245)) ([7cc4f65](googleapis@7cc4f65)) * **skills:** Generate skills offline without live source connections ([googleapis#3388](googleapis#3388)) ([4c860b6](googleapis@4c860b6)) * **skills:** Tolerate missing env vars during offline skills-generate ([googleapis#3399](googleapis#3399)) ([ea5d3e5](googleapis@ea5d3e5)) * **source/cloud-storage:** Restrict bucket and local path access ([googleapis#3454](googleapis#3454)) ([2c3ca5d](googleapis@2c3ca5d)) * **tools/bigquery:** Add per tool query label in BigQuery jobs ([googleapis#1975](googleapis#1975)) ([3f6a49f](googleapis@3f6a49f)) * **tools/dataplex:** Add tools to support metadata enrichment workflow ([googleapis#3270](googleapis#3270)) ([05289aa](googleapis@05289aa)) * **tools/mysql:** Add show-query-stats and list-all-locks tools for MySQL and Cloud SQL MySQL source ([googleapis#2954](googleapis#2954)) ([a9693bd](googleapis@a9693bd)) * **tools:** Decouple tool initialization from sources ([googleapis#3355](googleapis#3355)) ([32a24e3](googleapis@32a24e3)) ### Bug Fixes * **auth/dataplex:** Fix failing source with service account credentials ([googleapis#3369](googleapis#3369)) ([ba4deef](googleapis@ba4deef)) * **bigquery:** Wire maximumBytesBilled into prebuilt config ([googleapis#3385](googleapis#3385)) ([4abbf6e](googleapis@4abbf6e)) * Bound MCP HTTP body size ([googleapis#3216](googleapis#3216)) ([d4f4342](googleapis@d4f4342)) * **config:** Add doc/line context to parse errors ([googleapis#2957](googleapis#2957)) ([4b097da](googleapis@4b097da)) * Escape delimiter characters in applyEscape to prevent SQL injection ([googleapis#2811](googleapis#2811)) ([932519a](googleapis@932519a)) * **npm:** Source binary version from cmd/version.txt ([googleapis#3417](googleapis#3417)) ([6ffbdec](googleapis@6ffbdec)) * **prebuilt/alloydb-omni:** Require password env var explicitly ([googleapis#3398](googleapis#3398)) ([fcbe3e7](googleapis@fcbe3e7)) * **server:** Fail if MCP auth is enabled together with enable-api ([googleapis#3435](googleapis#3435)) ([a6ff910](googleapis@a6ff910)) * **server:** Return errors instead of panicking in InitializeConfigs ([googleapis#3397](googleapis#3397)) ([f48b01d](googleapis@f48b01d)) * **source/cloudhealthcare:** Validate pageURL parameter to prevent SSRF ([googleapis#3453](googleapis#3453)) ([9abf47d](googleapis@9abf47d)) * **source/dataplex,source/datalineage:** Specify cloud-platform scope for default credentials ([googleapis#3376](googleapis#3376)) ([13e8c36](googleapis@13e8c36)) * **source/http:** Implement SSRF guard ([googleapis#3448](googleapis#3448)) ([24d7d29](googleapis@24d7d29)) * **tool/bigquery-execute-sql:** Prevent dataset restriction bypass ([googleapis#3452](googleapis#3452)) ([ca6d5e3](googleapis@ca6d5e3)) * **tool/mysql-get-query-plan:** Prevent query execution bypass and statement injection ([googleapis#3235](googleapis#3235)) ([7ed1e7b](googleapis@7ed1e7b)) * **tool/spanner-sql,tool/spanner-execute-sql:** Use read-only annotations when readOnly is set ([googleapis#3338](googleapis#3338)) ([8bde0ec](googleapis@8bde0ec)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --------- Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Yuan Teoh <[email protected]> 45c0f86
🤖 I have created a release *beep* *boop* --- ## [1.5.0](googleapis/mcp-toolbox@v1.4.0...v1.5.0) (2026-06-18) ### Features * **auth/google:** Require audience or clientId for mcpEnabled ([googleapis#3450](googleapis#3450)) ([59f7b6e](googleapis@59f7b6e)) * Enable per source level flags for sql commenter ([googleapis#3465](googleapis#3465)) ([ecce6b7](googleapis@ecce6b7)) * **mcp:** Add URL parameter binding for HTTP transport ([googleapis#3112](googleapis#3112)) ([0cc7b37](googleapis@0cc7b37)) * **scylladb:** Adding support for ScyllaDB source and tool ([googleapis#3119](googleapis#3119)) ([2dada83](googleapis@2dada83)) * **server:** Add support for toolset filtering in prebuilt CLI flag ([googleapis#3245](googleapis#3245)) ([7cc4f65](googleapis@7cc4f65)) * **skills:** Generate skills offline without live source connections ([googleapis#3388](googleapis#3388)) ([4c860b6](googleapis@4c860b6)) * **skills:** Tolerate missing env vars during offline skills-generate ([googleapis#3399](googleapis#3399)) ([ea5d3e5](googleapis@ea5d3e5)) * **source/cloud-storage:** Restrict bucket and local path access ([googleapis#3454](googleapis#3454)) ([2c3ca5d](googleapis@2c3ca5d)) * **tools/bigquery:** Add per tool query label in BigQuery jobs ([googleapis#1975](googleapis#1975)) ([3f6a49f](googleapis@3f6a49f)) * **tools/dataplex:** Add tools to support metadata enrichment workflow ([googleapis#3270](googleapis#3270)) ([05289aa](googleapis@05289aa)) * **tools/mysql:** Add show-query-stats and list-all-locks tools for MySQL and Cloud SQL MySQL source ([googleapis#2954](googleapis#2954)) ([a9693bd](googleapis@a9693bd)) * **tools:** Decouple tool initialization from sources ([googleapis#3355](googleapis#3355)) ([32a24e3](googleapis@32a24e3)) ### Bug Fixes * **auth/dataplex:** Fix failing source with service account credentials ([googleapis#3369](googleapis#3369)) ([ba4deef](googleapis@ba4deef)) * **bigquery:** Wire maximumBytesBilled into prebuilt config ([googleapis#3385](googleapis#3385)) ([4abbf6e](googleapis@4abbf6e)) * Bound MCP HTTP body size ([googleapis#3216](googleapis#3216)) ([d4f4342](googleapis@d4f4342)) * **config:** Add doc/line context to parse errors ([googleapis#2957](googleapis#2957)) ([4b097da](googleapis@4b097da)) * Escape delimiter characters in applyEscape to prevent SQL injection ([googleapis#2811](googleapis#2811)) ([932519a](googleapis@932519a)) * **npm:** Source binary version from cmd/version.txt ([googleapis#3417](googleapis#3417)) ([6ffbdec](googleapis@6ffbdec)) * **prebuilt/alloydb-omni:** Require password env var explicitly ([googleapis#3398](googleapis#3398)) ([fcbe3e7](googleapis@fcbe3e7)) * **server:** Fail if MCP auth is enabled together with enable-api ([googleapis#3435](googleapis#3435)) ([a6ff910](googleapis@a6ff910)) * **server:** Return errors instead of panicking in InitializeConfigs ([googleapis#3397](googleapis#3397)) ([f48b01d](googleapis@f48b01d)) * **source/cloudhealthcare:** Validate pageURL parameter to prevent SSRF ([googleapis#3453](googleapis#3453)) ([9abf47d](googleapis@9abf47d)) * **source/dataplex,source/datalineage:** Specify cloud-platform scope for default credentials ([googleapis#3376](googleapis#3376)) ([13e8c36](googleapis@13e8c36)) * **source/http:** Implement SSRF guard ([googleapis#3448](googleapis#3448)) ([24d7d29](googleapis@24d7d29)) * **tool/bigquery-execute-sql:** Prevent dataset restriction bypass ([googleapis#3452](googleapis#3452)) ([ca6d5e3](googleapis@ca6d5e3)) * **tool/mysql-get-query-plan:** Prevent query execution bypass and statement injection ([googleapis#3235](googleapis#3235)) ([7ed1e7b](googleapis@7ed1e7b)) * **tool/spanner-sql,tool/spanner-execute-sql:** Use read-only annotations when readOnly is set ([googleapis#3338](googleapis#3338)) ([8bde0ec](googleapis@8bde0ec)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --------- Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Yuan Teoh <[email protected]> 45c0f86
🤖 I have created a release *beep* *boop* --- ## [1.5.0](googleapis/mcp-toolbox@v1.4.0...v1.5.0) (2026-06-18) ### Features * **auth/google:** Require audience or clientId for mcpEnabled ([googleapis#3450](googleapis#3450)) ([59f7b6e](googleapis@59f7b6e)) * Enable per source level flags for sql commenter ([googleapis#3465](googleapis#3465)) ([ecce6b7](googleapis@ecce6b7)) * **mcp:** Add URL parameter binding for HTTP transport ([googleapis#3112](googleapis#3112)) ([0cc7b37](googleapis@0cc7b37)) * **scylladb:** Adding support for ScyllaDB source and tool ([googleapis#3119](googleapis#3119)) ([2dada83](googleapis@2dada83)) * **server:** Add support for toolset filtering in prebuilt CLI flag ([googleapis#3245](googleapis#3245)) ([7cc4f65](googleapis@7cc4f65)) * **skills:** Generate skills offline without live source connections ([googleapis#3388](googleapis#3388)) ([4c860b6](googleapis@4c860b6)) * **skills:** Tolerate missing env vars during offline skills-generate ([googleapis#3399](googleapis#3399)) ([ea5d3e5](googleapis@ea5d3e5)) * **source/cloud-storage:** Restrict bucket and local path access ([googleapis#3454](googleapis#3454)) ([2c3ca5d](googleapis@2c3ca5d)) * **tools/bigquery:** Add per tool query label in BigQuery jobs ([googleapis#1975](googleapis#1975)) ([3f6a49f](googleapis@3f6a49f)) * **tools/dataplex:** Add tools to support metadata enrichment workflow ([googleapis#3270](googleapis#3270)) ([05289aa](googleapis@05289aa)) * **tools/mysql:** Add show-query-stats and list-all-locks tools for MySQL and Cloud SQL MySQL source ([googleapis#2954](googleapis#2954)) ([a9693bd](googleapis@a9693bd)) * **tools:** Decouple tool initialization from sources ([googleapis#3355](googleapis#3355)) ([32a24e3](googleapis@32a24e3)) ### Bug Fixes * **auth/dataplex:** Fix failing source with service account credentials ([googleapis#3369](googleapis#3369)) ([ba4deef](googleapis@ba4deef)) * **bigquery:** Wire maximumBytesBilled into prebuilt config ([googleapis#3385](googleapis#3385)) ([4abbf6e](googleapis@4abbf6e)) * Bound MCP HTTP body size ([googleapis#3216](googleapis#3216)) ([d4f4342](googleapis@d4f4342)) * **config:** Add doc/line context to parse errors ([googleapis#2957](googleapis#2957)) ([4b097da](googleapis@4b097da)) * Escape delimiter characters in applyEscape to prevent SQL injection ([googleapis#2811](googleapis#2811)) ([932519a](googleapis@932519a)) * **npm:** Source binary version from cmd/version.txt ([googleapis#3417](googleapis#3417)) ([6ffbdec](googleapis@6ffbdec)) * **prebuilt/alloydb-omni:** Require password env var explicitly ([googleapis#3398](googleapis#3398)) ([fcbe3e7](googleapis@fcbe3e7)) * **server:** Fail if MCP auth is enabled together with enable-api ([googleapis#3435](googleapis#3435)) ([a6ff910](googleapis@a6ff910)) * **server:** Return errors instead of panicking in InitializeConfigs ([googleapis#3397](googleapis#3397)) ([f48b01d](googleapis@f48b01d)) * **source/cloudhealthcare:** Validate pageURL parameter to prevent SSRF ([googleapis#3453](googleapis#3453)) ([9abf47d](googleapis@9abf47d)) * **source/dataplex,source/datalineage:** Specify cloud-platform scope for default credentials ([googleapis#3376](googleapis#3376)) ([13e8c36](googleapis@13e8c36)) * **source/http:** Implement SSRF guard ([googleapis#3448](googleapis#3448)) ([24d7d29](googleapis@24d7d29)) * **tool/bigquery-execute-sql:** Prevent dataset restriction bypass ([googleapis#3452](googleapis#3452)) ([ca6d5e3](googleapis@ca6d5e3)) * **tool/mysql-get-query-plan:** Prevent query execution bypass and statement injection ([googleapis#3235](googleapis#3235)) ([7ed1e7b](googleapis@7ed1e7b)) * **tool/spanner-sql,tool/spanner-execute-sql:** Use read-only annotations when readOnly is set ([googleapis#3338](googleapis#3338)) ([8bde0ec](googleapis@8bde0ec)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --------- Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> Co-authored-by: Yuan Teoh <[email protected]> 45c0f86
Summary
mysql-get-query-planbuilds its EXPLAIN query by directly interpolating thecaller-supplied
sql_statementintoEXPLAIN FORMAT=JSON %swith novalidation:
Attack 1 — EXPLAIN ANALYZE execution bypass
MySQL 8.0+ treats
EXPLAIN [FORMAT=JSON] ANALYZE <stmt>as an executionprimitive: the statement is actually run (with full I/O) and the measured
runtime statistics are returned. Supplying
causes the tool to issue
which MySQL executes. The
actual_rowsfield in the returned JSON is non-zerowhen the WHERE condition matches, enabling boolean-based blind extraction of
arbitrary data. Heavier variants (long
SLEEP, table scans on huge tables)yield denial-of-service.
Attack 2 — multi-statement injection via semicolons
Without semicolon filtering, a caller can append additional statements:
Many MySQL drivers execute each semicolon-separated statement in sequence.
Fix
ValidateSQLStatement(new, exported for testing):;.SELECT,INSERT,UPDATE,DELETE,REPLACE,TABLE,WITH,VALUES,FOR. This excludesANALYZE,DROP,CREATE,CALL, andall other non-explainable forms.
13 unit tests added (happy-path DML variants + all attack variants).
Test plan
go build ./internal/tools/mysql/mysqlgetqueryplan/...— cleango test ./internal/tools/mysql/mysqlgetqueryplan/... -run TestValidateSQLStatement -v— 13/13 PASSANALYZEpayload → rejected at input validation, never reachesRunSQLSELECTqueries → pass through unchanged