From 9ad4135f6958305cff74a5f833e787184f48ade8 Mon Sep 17 00:00:00 2001 From: Huang HaoCheng <2671715549@qq.com> Date: Wed, 4 Nov 2026 16:05:30 +0800 Subject: [PATCH] fix: apply the PostgreSQL dangerous-function denylist under the pg datasource type DS_SPECIFIC_DANGEROUS_FUNCTIONS is looked up with ds.type, and PostgreSQL datasources carry the DB enum value 'pg' (apps/db/constant.py), not 'postgresql'. The table was keyed 'postgresql', so pg_read_file, pg_write_file, lo_import and lo_export were never blocked for PostgreSQL datasources. Key the entry by the DB type, and cover the rest of the server-side file readers that belong to the same family (pg_read_binary_file, pg_ls_dir, pg_stat_file). Regression coverage drives check_sql_read instead of the table alone, pins every denylist key to a DB type so a mismatch cannot silently disable a dialect again, and checks that INSERT/UPDATE/DELETE/TRUNCATE/COPY/DROP stay rejected on PostgreSQL. Refs #1438 --- backend/apps/db/db.py | 13 +++- backend/tests/test_postgres_denylist.py | 96 +++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 backend/tests/test_postgres_denylist.py diff --git a/backend/apps/db/db.py b/backend/apps/db/db.py index edea1f61..02142959 100644 --- a/backend/apps/db/db.py +++ b/backend/apps/db/db.py @@ -1152,11 +1152,22 @@ def get_sqlglot_dialect(ds_type: str) -> str: COMMON_DANGEROUS_FUNCTIONS = {'version', 'current_user', 'user', 'database'} # 特定数据库的危险函数 +# key 必须与 DB 枚举的 type 值一致(见 apps/db/constant.py),例如 PostgreSQL 的 type 是 'pg', +# 而不是 'postgresql';写成后者会让该数据源的危险函数规则整条失效。 DS_SPECIFIC_DANGEROUS_FUNCTIONS = { 'mysql': {'LOAD_FILE', 'INTO OUTFILE', 'INTO DUMPFILE'}, 'doris': {'LOAD_FILE', 'INTO OUTFILE', 'INTO DUMPFILE'}, 'starrocks': {'LOAD_FILE', 'INTO OUTFILE', 'INTO DUMPFILE'}, - 'postgresql': {'pg_read_file', 'pg_write_file', 'lo_import', 'lo_export'}, + 'pg': { + # 服务端文件读写族:只读连接也可能借此读到数据库主机的本地文件 + 'pg_read_file', + 'pg_read_binary_file', + 'pg_ls_dir', + 'pg_stat_file', + 'pg_write_file', + 'lo_import', + 'lo_export', + }, 'sqlserver': {'EXEC', 'xp_cmdshell', 'sp_executesql'}, 'oracle': {'UTL_FILE', 'DBMS_PIPE', 'DBMS_LOCK'}, 'hive': {'ADD FILE', 'ADD JAR'}, diff --git a/backend/tests/test_postgres_denylist.py b/backend/tests/test_postgres_denylist.py new file mode 100644 index 00000000..c150a09f --- /dev/null +++ b/backend/tests/test_postgres_denylist.py @@ -0,0 +1,96 @@ +"""Regression coverage for the PostgreSQL dangerous-function denylist (issue 1438). + +`DS_SPECIFIC_DANGEROUS_FUNCTIONS` is looked up with `ds.type`, whose PostgreSQL +value is `pg` (see `apps/db/constant.py`). The table was keyed `postgresql`, so +none of the PostgreSQL dangerous functions were ever blocked. + +These tests drive the guard the datasource path actually uses (`check_sql_read`) +rather than the table alone, and pin the table keys to the `DB` type codes so a +mismatch cannot silently disable a dialect again. +""" + +from types import SimpleNamespace + +import pytest + +from apps.db.constant import DB +from apps.db.db import ( + COMMON_DANGEROUS_FUNCTIONS, + DS_SPECIFIC_DANGEROUS_FUNCTIONS, + check_sql_read, + get_dangerous_functions, +) + +PG_FILE_FUNCTIONS = { + "pg_read_file", + "pg_read_binary_file", + "pg_ls_dir", + "pg_stat_file", + "pg_write_file", + "lo_import", + "lo_export", +} + + +def _ds(db: DB) -> SimpleNamespace: + return SimpleNamespace(type=db.type) + + +def test_every_dialect_key_is_a_db_type(): + """A key that is not a `DB.type` value silently disables that dialect's rules.""" + known = {db.type.lower() for db in DB} + unknown = set(DS_SPECIFIC_DANGEROUS_FUNCTIONS) - known + assert not unknown, f"denylist keys that no DB type uses: {sorted(unknown)}" + + +def test_postgres_functions_are_resolved_from_the_pg_type(): + functions = get_dangerous_functions(DB.pg.type) + assert PG_FILE_FUNCTIONS <= functions + assert COMMON_DANGEROUS_FUNCTIONS <= functions + + +@pytest.mark.parametrize( + "sql", + [ + "SELECT pg_read_file('/etc/passwd')", + "SELECT pg_read_binary_file('/etc/shadow')", + "SELECT pg_ls_dir('/')", + "SELECT pg_stat_file('/etc/passwd')", + "SELECT pg_write_file('/tmp/out', 'x')", + "SELECT lo_import('/etc/passwd')", + "SELECT lo_export(1, '/tmp/out')", + ], +) +def test_postgres_file_functions_are_rejected(sql): + is_safe, reason = check_sql_read(sql, _ds(DB.pg)) + assert not is_safe + assert "dangerous function" in reason + + +@pytest.mark.parametrize( + "sql", + [ + "INSERT INTO users (id) VALUES (1)", + "UPDATE users SET name = 'x'", + "DELETE FROM users", + "TRUNCATE TABLE users", + "COPY users FROM STDIN", + "DROP TABLE users", + ], +) +def test_postgres_write_statements_are_rejected(sql): + is_safe, _ = check_sql_read(sql, _ds(DB.pg)) + assert not is_safe + + +def test_ordinary_postgres_select_is_still_allowed(): + is_safe, reason = check_sql_read("SELECT id, name FROM users", _ds(DB.pg)) + assert is_safe, reason + + +def test_other_dialects_keep_their_own_rules(): + assert "xp_cmdshell" in get_dangerous_functions(DB.sqlServer.type) + # A MySQL-only entry must not leak into the PostgreSQL list. + assert "LOAD_FILE" not in get_dangerous_functions(DB.pg.type) + # ...and a PostgreSQL-only entry must not leak into the MySQL list. + assert "pg_read_file" not in get_dangerous_functions(DB.mysql.type)