Skip to content

Commit fd3d334

Browse files
committed
fix: retain SQL table extraction when skipping whitelist checks
1 parent 6cb69d0 commit fd3d334

5 files changed

Lines changed: 46 additions & 28 deletions

File tree

‎backend/README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44

55
`SQLBOT_SKIP_TABLE_CHECK` 默认为 `false`。不配置时,生成的 SQL 仍通过真实表名提取和本轮允许表集合检查。
66

7-
仅在需要跳过这组检查时设置:
7+
仅在需要跳过表白名单检查时设置:
88

99
```dotenv
1010
SQLBOT_SKIP_TABLE_CHECK=true
1111
```
1212

1313
通过进程环境变量传入;本地开发也可写入仓库根目录 `.env`,安装版可写入安装目录的 `conf/sqlbot.conf`。Docker Compose 部署可在 SQLBot 服务的 `environment` 中设置。修改后需重启服务或重建容器,所有问数入口共用该启动配置,不提供配置页面或运行时修改。
1414

15-
开启后跳过表名提取、无法提取表名时的拒绝及表白名单检查,可能允许查询数据源账号可读取的未勾选表。SQL 只读、危险函数、元数据查询控制、行权限处理及行数限制仍按原有流程执行。
15+
开启后仅跳过表白名单检查,可能允许查询数据源账号可读取的未勾选表。真实表名提取始终执行,无法提取表名时仍拒绝 SQL。SQL 只读、危险函数、元数据查询控制、行权限处理及行数限制仍按原有流程执行。

‎backend/apps/chat/task/llm.py‎

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1578,21 +1578,20 @@ def run_task(self, in_chat: bool = True, stream: bool = True,
15781578
sql_operate = OperationEnum.GENERATE_SQL
15791579
sql, tables = self.check_sql(session=_session, res=full_sql_text, operate=sql_operate)
15801580

1581-
if not settings.SQLBOT_SKIP_TABLE_CHECK:
1582-
# 表名安全检查:用 sqlglot 解析真实 SQL,不信任 AI 返回的 tables
1583-
actual_tables = extract_tables_from_sql(sql, ds_type=self.ds.type)
1584-
if not actual_tables:
1585-
raise SingleMessageError(
1586-
"SQL parsing failed: unable to extract table names. "
1587-
"This may indicate an unsupported SQL syntax or a security issue."
1588-
)
1589-
allowed_tables = set(self.table_name_list)
1590-
unauthorized_tables = actual_tables - allowed_tables
1591-
if unauthorized_tables:
1592-
raise SingleMessageError(
1593-
f"SQL contains unauthorized tables: {', '.join(unauthorized_tables)}. "
1594-
f"Allowed tables: {', '.join(allowed_tables)}"
1595-
)
1581+
# 表名安全检查:用 sqlglot 解析真实 SQL,不信任 AI 返回的 tables
1582+
actual_tables = extract_tables_from_sql(sql, ds_type=self.ds.type)
1583+
if not actual_tables:
1584+
raise SingleMessageError(
1585+
"SQL parsing failed: unable to extract table names. "
1586+
"This may indicate an unsupported SQL syntax or a security issue."
1587+
)
1588+
allowed_tables = set(self.table_name_list)
1589+
unauthorized_tables = actual_tables - allowed_tables
1590+
if unauthorized_tables and not settings.SQLBOT_SKIP_TABLE_CHECK:
1591+
raise SingleMessageError(
1592+
f"SQL contains unauthorized tables: {', '.join(unauthorized_tables)}. "
1593+
f"Allowed tables: {', '.join(allowed_tables)}"
1594+
)
15961595

15971596
if ((not self.current_assistant or is_page_embedded) and is_normal_user(
15981597
self.current_user)) or use_dynamic_ds:

‎backend/common/core/config.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def SQLALCHEMY_DATABASE_URI(self) -> PostgresDsn | str:
117117
# 默认关闭,防止通过元数据查询泄露数据库结构
118118
SQLBOT_ALLOW_METADATA_QUERIES: bool = False
119119

120-
# 启动配置:显式开启时跳过生成 SQL 的表名提取和白名单检查
120+
# 启动配置:显式开启时跳过生成 SQL 的表白名单检查,仍须成功提取表名
121121
SQLBOT_SKIP_TABLE_CHECK: bool = False
122122

123123
PARSE_REASONING_BLOCK_ENABLED: bool = True

‎backend/tests/test_table_check.py‎

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,6 @@ def test_default_allows_known_tables_and_cte_aliases(task, sql):
9292
"SELECT id FROM sales UNION ALL SELECT id FROM secret_payroll",
9393
"unauthorized tables: secret_payroll",
9494
),
95-
("SELECT 1", "unable to extract table names"),
96-
("SELECT (", "unable to extract table names"),
9795
],
9896
)
9997
def test_default_rejects_invalid_or_unknown_tables_before_save_and_execute(
@@ -107,16 +105,37 @@ def test_default_rejects_invalid_or_unknown_tables_before_save_and_execute(
107105
task.execute_sql.assert_not_called()
108106

109107

110-
@pytest.mark.parametrize("sql", ["SELECT * FROM secret_payroll", "SELECT 1"])
111-
def test_explicit_skip_bypasses_extraction_and_keeps_remaining_flow(
108+
@pytest.mark.parametrize("skip", [False, True])
109+
@pytest.mark.parametrize("sql", ["SELECT 1", "SELECT ("])
110+
def test_missing_tables_are_rejected_even_when_whitelist_check_is_skipped(
111+
task, monkeypatch, sql, skip
112+
):
113+
monkeypatch.setattr(llm.settings, "SQLBOT_SKIP_TABLE_CHECK", skip)
114+
result = run(task, sql)
115+
assert result["success"] is False
116+
assert "unable to extract table names" in result["message"]
117+
task.save_error.assert_called_once()
118+
task.generate_filter.assert_not_called()
119+
task.check_save_sql.assert_not_called()
120+
task.execute_sql.assert_not_called()
121+
122+
123+
@pytest.mark.parametrize(
124+
"sql",
125+
[
126+
"SELECT * FROM secret_payroll",
127+
"SELECT id FROM sales UNION ALL SELECT id FROM secret_payroll",
128+
],
129+
)
130+
def test_explicit_skip_only_bypasses_whitelist_check_and_keeps_remaining_flow(
112131
task, monkeypatch, sql
113132
):
114133
monkeypatch.setattr(llm.settings, "SQLBOT_SKIP_TABLE_CHECK", True)
115-
extract = Mock(side_effect=AssertionError("table extraction must be skipped"))
134+
extract = Mock(wraps=llm.extract_tables_from_sql)
116135
monkeypatch.setattr(llm, "extract_tables_from_sql", extract)
117136
result = run(task, sql)
118137
assert result["success"] is True
119-
extract.assert_not_called()
138+
extract.assert_called_once_with(sql, ds_type="pg")
120139
task.generate_filter.assert_called_once()
121140
task.check_save_sql.assert_called_once()
122141
task.execute_sql.assert_called_once_with(sql=sql)
@@ -127,8 +146,8 @@ def test_explicit_skip_bypasses_extraction_and_keeps_remaining_flow(
127146
"sql",
128147
[
129148
"DELETE FROM sales",
130-
"SELECT pg_read_file('/etc/passwd')",
131-
"SHOW TABLES",
149+
"SELECT pg_read_file('/etc/passwd') FROM sales",
150+
"DESCRIBE sales",
132151
],
133152
)
134153
def test_skip_keeps_database_execution_guards(task, monkeypatch, sql):

‎docs/agents/security.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@
3232

3333
1. 只把允许的表和字段元数据提供给模型;
3434
2. 解析模型返回 JSON 并提取 SQL;
35-
3. 用 `sqlglot` 提取真实表名并排除 CTE 别名;
36-
4. 默认对照允许表集合拒绝未授权表;部署显式设置 `SQLBOT_SKIP_TABLE_CHECK=true` 时,跳过第 3、4 步及无法提取表名时的拒绝;
35+
3. 用 `sqlglot` 提取真实表名并排除 CTE 别名,无法提取表名时始终拒绝;
36+
4. 默认对照允许表集合拒绝未授权表;部署显式设置 `SQLBOT_SKIP_TABLE_CHECK=true` 时,仅跳过本步;
3737
5. 按用户、助手和页面嵌入场景应用行权限或动态 SQL 变换;
3838
6. 校验并保存最终 SQL;
3939
7. 执行最终 SQL 并保存结果。

0 commit comments

Comments
 (0)