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.