You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
security(mcp): McpSqlGuard function denylist is bypassed by a double-quoted function name #1769
`McpSqlGuard` (lib/core/mcp/mcp_sql_guard.dart) refuses server functions with the regex `_forbiddenFunctions`, which expects `NAME\s*(`. In PostgreSQL a quoted lower-case name is the same function, and `stripCommentsAndStrings` leaves double-quoted identifiers in the text, so the closing quote sits between the name and the parenthesis and the regex does not match:
```
SELECT pg_catalog."pg_terminate_backend"(123) -- passes the guard
SELECT "nextval"('s') -- passes the guard
```
(Checked on a copy of the regex: the unquoted forms are refused, the quoted forms are allowed. The guard upper-cases the text before matching, which does not matter for the bypass.)
The other layers do not cover this: `pg_terminate_backend` and `nextval` work in a read-only transaction, so the read-only session does not stop them; the rest depends on the connection access policy and the role's privileges. The same applies to `LO_`, `PG_READ_FILE`, `PG_SLEEP`, `DBLINK*` and the other names in the list.
Fix
Match the function list after removing the quotes of identifiers (replace `"name"` by `name` in a copy used for the denylist check), or tokenise the statement and compare identifier tokens followed by `(`.
Better: also add an allowlist for functions? At least extend the denylist for MySQL (`SLEEP`, `BENCHMARK`, `GET_LOCK`, `RELEASE_LOCK`, `IS_FREE_LOCK`) and PostgreSQL (`PG_STAT_RESET*`, `PG_SWITCH_WAL`, `PG_CREATE_RESTORE_POINT`, `PG_LOGICAL_EMIT_MESSAGE`, `PG_PROMOTE`, `PG_DROP_REPLICATION_SLOT`).
Tests: every denylisted function in unquoted, schema-qualified, quoted and spaced forms.
Found in the review of the SQL safety layer (see #1764 for the error-handling epic; this is separate).
Worse than a single function: SET_CONFIG is on the same denylist, so SELECT pg_catalog."set_config"('default_transaction_read_only','off',false) also passes the guard. That setting is the second read-only layer of the MCP session (createReadOnlyMcpDelegate: autocommit, pooled slot), so a quoted call can switch the layer off for the following calls. Later statements still go through the guard (data-modifying CTEs and INTO are refused), but a function with side effects would then be able to write. The fix should cover the quoted form for the whole list, and the MCP slot should reset the setting when it is taken from the pool.
Activity
ZhuchkaTriplesix commentedon Oct 25, 2026
Worse than a single function:
SET_CONFIGis on the same denylist, soSELECT pg_catalog."set_config"('default_transaction_read_only','off',false)also passes the guard. That setting is the second read-only layer of the MCP session (createReadOnlyMcpDelegate: autocommit, pooled slot), so a quoted call can switch the layer off for the following calls. Later statements still go through the guard (data-modifying CTEs andINTOare refused), but a function with side effects would then be able to write. The fix should cover the quoted form for the whole list, and the MCP slot should reset the setting when it is taken from the pool.Merge pull request #1782 from QueryaHub/fix/mcp-sql-guard-quoted-func…
ZhuchkaTriplesix commentedon Oct 25, 2026
Fixed on dev in #1782, #1797 (merged). Closing by hand: the PRs target dev, so GitHub would only close this when dev is merged into main.