fix(database-operation): 拦截未显式授权的凭据配置表读取

systemconfig 按 SystemConfigKey 聚合实例级 API Key、Cookie 与口令,托管
config.system.get 默认脱敏,但数据库技能脚本与 MCP database_operation 接受
任意 SQL,一条 SELECT 就能整包取回明文。

现在由脚本本身按表拦截:引用 systemconfig 的语句(含引号标识符、模式限定名、
CTE 与子查询)在读写两种模式下都默认拒绝,只有显式 --show-secrets /
show_secrets=true 才放行,失败信息直接给出可执行的下一步,因此 MCP 工具、
技能调用与命令行入口共享同一道边界。宿主策略把开启该开关的查询判定为
SENSITIVE_READ:要求系统管理员确认,结果标记为 SECRET,回执摘要不再保留内容。

门禁范围收敛为单表:userconfig 只承载界面偏好,plugindata 是文档化的诊断路径,
其余表的凭据残留风险改由 SKILL.md 的窄投影要求约束,避免以不可绕过的方式破坏
既有可运维性。MCP 文档、技能说明与相关契约测试同步更新。
This commit is contained in:
JontyLee
2026-10-26 20:20:16 +08:00
parent a11bc444f4
commit 0ba73f5b87
11 changed files with 423 additions and 35 deletions

View File

@@ -8,6 +8,9 @@ from pathlib import Path
from types import ModuleType
from typing import Any
import pytest
from sqlalchemy import create_engine, text
PROJECT_ROOT = Path(__file__).resolve().parents[1]
MP_DB_SCRIPT = PROJECT_ROOT / "skills" / "database-operation" / "scripts" / "mp-db.py"
@@ -117,11 +120,19 @@ def test_mp_db_write_command_allows_write_statement(monkeypatch) -> None:
module = _load_script(MP_DB_SCRIPT, "mp_db_script_write")
calls = []
def fake_run_query(sql: str, *, limit: int = 100, allow_write: bool = False) -> int:
def fake_run_query(
sql: str,
*,
limit: int = 100,
allow_write: bool = False,
show_secrets: bool = False,
) -> int:
"""
记录 write 子命令传入的执行参数。
"""
calls.append({"sql": sql, "limit": limit, "allow_write": allow_write})
calls.append(
{"sql": sql, "limit": limit, "allow_write": allow_write, "show_secrets": show_secrets}
)
return 0
monkeypatch.setattr(module, "run_query", fake_run_query)
@@ -141,5 +152,123 @@ def test_mp_db_write_command_allows_write_statement(monkeypatch) -> None:
"sql": "UPDATE subscribe SET state = 'S' WHERE id = 123",
"limit": 0,
"allow_write": True,
"show_secrets": False,
}
]
@pytest.mark.parametrize(
"sql",
[
"SELECT * FROM systemconfig",
'SELECT * FROM "systemconfig"',
"SELECT * FROM public.systemconfig",
"SELECT * FROM `systemconfig`",
"SELECT * FROM [systemconfig]",
"WITH leak AS (SELECT value FROM systemconfig) SELECT * FROM leak",
"SELECT (SELECT value FROM systemconfig LIMIT 1) AS any_value",
],
)
def test_mp_db_blocks_configuration_tables_without_show_secrets(monkeypatch, sql) -> None:
"""
引用凭据配置表的语句默认拒绝,引号、模式限定、子查询与写入模式都不能绕过。
"""
module = _load_script(MP_DB_SCRIPT, "mp_db_script_secret_blocks")
assert not module._is_supported_statement(sql, False)
assert module._is_supported_statement(sql, False, show_secrets=True)
assert not module._is_supported_statement(sql, True)
@pytest.mark.parametrize(
"sql",
[
"SELECT 'systemconfig' AS name FROM subscribe",
"-- systemconfig\nSELECT 1",
"SELECT 1 /* systemconfig */",
"SELECT name FROM subscribe WHERE name = 'systemconfig'",
"SELECT * FROM userconfig",
"SELECT plugin_id, key FROM plugindata",
],
)
def test_mp_db_configuration_gate_ignores_literals_and_unrelated_tables(
monkeypatch,
sql,
) -> None:
"""
字面值与注释中的表名不触发凭据门禁;门禁只覆盖集中存放实例凭据的 systemconfig,
其它表的普通读取保持原样。
"""
module = _load_script(MP_DB_SCRIPT, "mp_db_script_secret_allows")
assert module._is_supported_statement(sql, False)
def test_mp_db_configuration_table_access_reports_and_requires_explicit_flag(
tmp_path,
monkeypatch,
capsys,
) -> None:
"""
凭据配置表的读取与写入在未显式开启时被拒绝并给出可执行提示,开启后按原值返回。
"""
module = _load_script(MP_DB_SCRIPT, "mp_db_script_secret_exec")
engine = create_engine("sqlite:///" + str(tmp_path / "secret.db"))
module.run_query.__globals__["_build_engine"] = lambda: engine
try:
with engine.begin() as connection:
connection.execute(text("CREATE TABLE systemconfig (id INTEGER, key TEXT, value JSON)"))
connection.execute(
text("INSERT INTO systemconfig VALUES (1, 'TMDB_API_KEY', 'plain-secret')")
)
assert module.run_query("SELECT value FROM systemconfig") == 1
assert "require --show-secrets" in capsys.readouterr().err
assert module.run_query("UPDATE systemconfig SET value = 'x' WHERE id = 1", allow_write=True) == 1
assert "require --show-secrets" in capsys.readouterr().err
assert module.run_query("SELECT value FROM systemconfig", show_secrets=True) == 0
assert json.loads(capsys.readouterr().out)["rows"] == [{"value": "plain-secret"}]
assert (
module.run_query(
"UPDATE systemconfig SET value = 'fixed' WHERE id = 1",
allow_write=True,
show_secrets=True,
)
== 0
)
finally:
engine.dispose()
@pytest.mark.parametrize(
("command", "argv", "expected"),
[
("query", ["query", "SELECT 1", "--show-secrets"], True),
("query", ["query", "SELECT 1"], False),
("write", ["write", "UPDATE systemconfig SET value = 'x' WHERE id = 1", "--show-secrets"], True),
],
)
def test_mp_db_show_secrets_option_reaches_execution(monkeypatch, command, argv, expected) -> None:
"""
两个子命令的 --show-secrets 选项都必须映射到执行参数,缺省保持关闭。
"""
module = _load_script(MP_DB_SCRIPT, "mp_db_script_secret_option")
calls = []
def fake_run_query(
sql: str,
*,
limit: int = 100,
allow_write: bool = False,
show_secrets: bool = False,
) -> int:
"""
记录子命令解析出的凭据表开关。
"""
calls.append(show_secrets)
return 0
monkeypatch.setattr(module, "run_query", fake_run_query)
monkeypatch.setattr(module.sys, "argv", ["mp-db.py", *argv])
assert module.main() == 0
assert calls == [expected]