Skip to content

security(mcp): McpSqlGuard function denylist is bypassed by a double-quoted function name #1769

Closed

Description

@ZhuchkaTriplesix

Problem

`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).

Activity

  1. added
    bugSomething isn't working
    P1High priority / Core capability
    securitySecurity related
    on Oct 25, 2026
  2. ZhuchkaTriplesix commented on Oct 25, 2026

    @ZhuchkaTriplesix
    MemberAuthor

    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.

  3. added a commit that references this issue on Oct 25, 2026
  4. ZhuchkaTriplesix commented on Oct 25, 2026

    @ZhuchkaTriplesix
    MemberAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High priority / Core capabilitybugSomething isn't workingsecuritySecurity related

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions