Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions lib/core/mcp/mcp_sql_guard.dart
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ abstract final class McpSqlGuard {
r'PG_READ_FILE|PG_READ_BINARY_FILE|PG_LS_DIR|PG_STAT_FILE|LO_\w*|'
r'QUERY_TO_XML\w*|CURSOR_TO_XML|TABLE_TO_XML\w*|PG_(?:TRY_)?ADVISORY_\w*|PG_SLEEP\w*|'
r'NEXTVAL|SETVAL|DBLINK\w*|SET_CONFIG|LOAD_FILE|LOAD_EXTENSION|WRITEFILE|'
r'READFILE|FSDIR)\s*\(',
r'READFILE|FSDIR|SLEEP|BENCHMARK|GET_LOCK|RELEASE_LOCK|RELEASE_ALL_LOCKS|IS_FREE_LOCK|'
r'PG_STAT_RESET\w*|PG_SWITCH_WAL|PG_CREATE_RESTORE_POINT|PG_LOGICAL_EMIT_MESSAGE|'
r'PG_PROMOTE|PG_DROP_REPLICATION_SLOT)\s*\(',
);

/// SQLite pragmas that only read the schema.
Expand Down Expand Up @@ -114,7 +116,9 @@ abstract final class McpSqlGuard {
if (first == null || !_readStarts.contains(first)) {
return const McpSqlRefusal('read_only_only', 'Only read-only queries are allowed (SELECT, WITH, EXPLAIN, SHOW, DESCRIBE).');
}
if (_forbiddenFunctions.hasMatch(upper)) {
// `"nextval"('s')` is the same call in PostgreSQL; the stripper keeps
// quoted identifiers, so drop the quotes before looking for the name.
if (_forbiddenFunctions.hasMatch(upper.replaceAll(RegExp(r'["`]'), ''))) {
return const McpSqlRefusal('function_not_allowed', 'This query calls a server function that is not allowed over MCP.');
}
if (first == 'EXPLAIN') {
Expand Down
18 changes: 18 additions & 0 deletions test/core/mcp/mcp_sql_guard_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,24 @@ void main() {
refused("SELECT setval('s', 10)");
});

test('a denylisted function is refused quoted, qualified and spaced (#1769)',
() {
refused('SELECT pg_catalog."pg_terminate_backend"(123)');
refused('''SELECT "nextval"('s')''');
refused('SELECT "pg_sleep" (60)');
refused('SELECT pg_catalog.pg_sleep (60)');
refused('SELECT pg_sleep /* c */ (60)');
refused('SELECT pg_stat_reset()');
refused("SELECT pg_create_restore_point('x')");
refused('SELECT pg_promote()');
refused('SELECT SLEEP(5)', SqlDialect.mysql);
refused("SELECT BENCHMARK(1000000, MD5('a'))", SqlDialect.mysql);
refused("SELECT GET_LOCK('l', 10)", SqlDialect.mysql);
refused("SELECT RELEASE_LOCK('l')", SqlDialect.mysql);
refused("SELECT IS_FREE_LOCK('l')", SqlDialect.mysql);
refused('SELECT `sleep`(5)', SqlDialect.mysql);
});

test('EXPLAIN of a SELECT that calls REPLACE() is allowed (#1565)', () {
allowed("EXPLAIN SELECT REPLACE(name, 'a', 'b') FROM users");
refused("EXPLAIN REPLACE INTO users VALUES (1)", SqlDialect.mysql);
Expand Down
Loading