You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/10/19 22:38:01 UTC
(superset) branch master updated: fix: add dblink and pg_* state-mutating functions to SQL denylist (#45089)
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 5a1831000a9 fix: add dblink and pg_* state-mutating functions to SQL denylist (#45089)
5a1831000a9 is described below
commit 5a1831000a92466898f1822aee33210bb725c1f0
Author: lukegranto23 <lu...@gmail.com>
AuthorDate: Mon Oct 19 18:37:51 2026 -0400
fix: add dblink and pg_* state-mutating functions to SQL denylist (#45089)
Co-authored-by: lukegranto23 <se...@gmail.com>
Co-authored-by: Claude Opus 4.6 <no...@anthropic.com>
---
superset/config.py | 16 +++++++++++++
superset/sql/parse.py | 16 +++++++++++++
tests/unit_tests/commands/sql_lab/test_estimate.py | 27 ++++++++++++++++++++++
tests/unit_tests/sql/parse_tests.py | 15 ++++++++++++
4 files changed, 74 insertions(+)
diff --git a/superset/config.py b/superset/config.py
index ceb47817f78..bf15334e8b7 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -2535,6 +2535,22 @@ DISALLOWED_SQL_FUNCTIONS: dict[str, set[str]] = {
# Other potentially dangerous functions
"pg_sleep",
"pg_terminate_backend",
+ # dblink functions can open separate connections and auto-commit
+ # writes, bypassing the read-only gate entirely
+ "dblink",
+ "dblink_exec",
+ "dblink_connect",
+ "dblink_connect_u",
+ "dblink_send_query",
+ # State-mutating functions callable via SELECT
+ "pg_cancel_backend",
+ "pg_reload_conf",
+ "pg_rotate_logfile",
+ "pg_stat_reset",
+ "pg_switch_wal",
+ "pg_logical_emit_message",
+ "pg_create_restore_point",
+ "pg_drop_replication_slot",
},
# MySQL functions and variables that could reveal sensitive information
"mysql": {
diff --git a/superset/sql/parse.py b/superset/sql/parse.py
index 662b3659575..0ce5b2f23e1 100644
--- a/superset/sql/parse.py
+++ b/superset/sql/parse.py
@@ -1055,6 +1055,22 @@ class SQLStatement(BaseSQLStatement[exp.Expression]):
# last value, so it is intentionally not listed.)
"SETVAL",
"NEXTVAL",
+ # dblink functions open a separate connection and auto-commit,
+ # so writes persist even when the outer transaction is read-only.
+ "DBLINK",
+ "DBLINK_EXEC",
+ "DBLINK_CONNECT",
+ "DBLINK_CONNECT_U",
+ "DBLINK_SEND_QUERY",
+ # PostgreSQL state-mutating functions callable via SELECT
+ "PG_CANCEL_BACKEND",
+ "PG_RELOAD_CONF",
+ "PG_ROTATE_LOGFILE",
+ "PG_STAT_RESET",
+ "PG_SWITCH_WAL",
+ "PG_LOGICAL_EMIT_MESSAGE",
+ "PG_CREATE_RESTORE_POINT",
+ "PG_DROP_REPLICATION_SLOT",
}
)
diff --git a/tests/unit_tests/commands/sql_lab/test_estimate.py b/tests/unit_tests/commands/sql_lab/test_estimate.py
index b34cca55eef..55af58cf1b0 100644
--- a/tests/unit_tests/commands/sql_lab/test_estimate.py
+++ b/tests/unit_tests/commands/sql_lab/test_estimate.py
@@ -310,6 +310,33 @@ def test_apply_sql_security_blocks_disallowed_function(mock_app: MagicMock) -> N
command._apply_sql_security("SELECT pg_sleep(1)")
+@pytest.mark.parametrize(
+ "sql",
+ [
+ "SELECT pg_stat_reset()",
+ "SELECT dblink_exec('host=h dbname=d', 'DELETE FROM t')",
+ "SELECT dblink_connect_u('c', 'host=h dbname=d')",
+ "SELECT dblink_send_query('c', 'DELETE FROM t')",
+ "SELECT pg_cancel_backend(1234)",
+ ],
+)
+@patch("superset.commands.sql_lab.estimate.app")
+def test_apply_sql_security_blocks_shipped_default_functions(
+ mock_app: MagicMock, sql: str
+) -> None:
+ """Shipped DISALLOWED_SQL_FUNCTIONS catches dblink and pg_* state-mutators."""
+ from superset.config import DISALLOWED_SQL_FUNCTIONS
+ from superset.exceptions import SupersetDisallowedSQLFunctionException
+
+ mock_app.config = {
+ "DISALLOWED_SQL_FUNCTIONS": DISALLOWED_SQL_FUNCTIONS,
+ "DISALLOWED_SQL_TABLES": {},
+ }
+ command = _make_command_with_db(sql, allow_dml=True)
+ with pytest.raises(SupersetDisallowedSQLFunctionException):
+ command._apply_sql_security(sql)
+
+
@patch("superset.commands.sql_lab.estimate.app")
def test_apply_sql_security_allows_benign_select(mock_app: MagicMock) -> None:
"""A benign statement passes through unchanged (no false positives)."""
diff --git a/tests/unit_tests/sql/parse_tests.py b/tests/unit_tests/sql/parse_tests.py
index fe15fec9c0a..d6a17f194f5 100644
--- a/tests/unit_tests/sql/parse_tests.py
+++ b/tests/unit_tests/sql/parse_tests.py
@@ -2384,6 +2384,21 @@ def test_is_mutating_anonymous_block(sql: str, expected: bool) -> None:
("SELECT nextval('public.my_seq')", True),
# currval() only reads the session's last value, so it is not mutating.
("SELECT currval('public.my_seq')", False),
+ # dblink functions that open remote connections or dispatch async writes.
+ ("SELECT dblink_exec('host=h dbname=d', 'DELETE FROM t')", True),
+ ("SELECT dblink_connect('myconn', 'host=h dbname=d')", True),
+ ("SELECT dblink_connect_u('myconn', 'host=h dbname=d')", True),
+ ("SELECT dblink_send_query('myconn', 'DELETE FROM t')", True),
+ ("SELECT * FROM dblink('host=h dbname=d', 'SELECT 1') AS t(c int)", True),
+ # pg_* state-mutating functions callable via SELECT.
+ ("SELECT pg_cancel_backend(1234)", True),
+ ("SELECT pg_reload_conf()", True),
+ ("SELECT pg_stat_reset()", True),
+ ("SELECT pg_switch_wal()", True),
+ ("SELECT pg_logical_emit_message(true, 'channel', 'msg')", True),
+ ("SELECT pg_create_restore_point('rp1')", True),
+ ("SELECT pg_drop_replication_slot('slot1')", True),
+ ("SELECT pg_rotate_logfile()", True),
# Read-side large-object functions are intentionally NOT classified
# as mutating here. They are still blocked via the function denylist
# (see DISALLOWED_SQL_FUNCTIONS) but they do not write state.