fix(backend): Wave 1 Day 1 三个 P0 D Bug 修复
- W1-T3 修 4 处 fdw_etl.* 必坏残留 → app.* (P0-5 致命 1) · tenant_users.py L431/L456-457: v_dim_assistant + v_dim_staff(_ex) · tenant_excel.py L394/L411: v_dim_assistant + v_dim_staff · tenant_clues.py L119: v_dim_member · 修复后 tenant-admin 用户审核 / Excel 上传 / 维客线索恢复正常 - W1-T4 JWT aud sign 端写入 (P0-5 致命 2 最小止血) · jwt.py 全部 token 创建/解码函数加 audience 参数 · auth.py admin 端加 audience="admin" · xcx_auth.py miniapp 端加 audience="miniapp" (8 处调用) · 18 router 切强制 aud 校验留 Wave 2 - W1-T5 DBViewer 白名单 + 黑名单双保险 (P0-8) · 白名单: SELECT/WITH/EXPLAIN/SHOW 开头 · 黑名单: 17 关键词覆盖全 DML/DDL/DCL · 注释剥离避免误伤;15/15 单测 PASS 参考: docs/audit/changes/2026-05-04__wave1_day1_d_bug_triple_fix.md
This commit is contained in:
@@ -33,12 +33,36 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/api/db", tags=["数据库查看器"])
|
||||
|
||||
# 写操作关键词(不区分大小写)
|
||||
_WRITE_KEYWORDS = re.compile(
|
||||
r"\b(INSERT|UPDATE|DELETE|DROP|TRUNCATE)\b",
|
||||
# P0-8 修复(2026-05-04 Wave 1):改为白名单 + 黑名单双保险
|
||||
# 白名单:SQL 必须以这些关键词开头(去注释/空白后)
|
||||
_ALLOWED_PREFIXES = ("SELECT", "WITH", "EXPLAIN", "SHOW")
|
||||
|
||||
# 黑名单:深度防御,即使开头通过,语句中包含这些关键词也拒绝
|
||||
# 涵盖 DML / DDL / DCL,补全原仅 5 个关键词的漏洞
|
||||
_DENY_KEYWORDS = re.compile(
|
||||
r"\b(INSERT|UPDATE|DELETE|DROP|TRUNCATE|ALTER|CREATE|GRANT|REVOKE|COPY|CALL|COMMENT|VACUUM|REINDEX|CLUSTER|REFRESH|LOCK)\b",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
def _strip_comments(sql: str) -> str:
|
||||
"""剥离 SQL 中的 -- 行注释 与 /* */ 块注释,避免黑名单误匹配注释里的英文词。"""
|
||||
# 块注释 /* ... */ (非贪婪)
|
||||
sql = re.sub(r"/\*.*?\*/", " ", sql, flags=re.DOTALL)
|
||||
# 行注释 -- 直到行尾
|
||||
sql = re.sub(r"--[^\n]*", " ", sql)
|
||||
return sql
|
||||
|
||||
|
||||
def _extract_first_keyword(sql: str) -> str:
|
||||
"""提取 SQL 第一个有效关键词(去掉前导注释 + 空白)。
|
||||
|
||||
支持 -- 行注释 和 /* */ 块注释。返回大写关键词,无关键词返回空字符串。
|
||||
"""
|
||||
s = _strip_comments(sql).strip()
|
||||
m = re.match(r"\s*([A-Za-z_]+)", s)
|
||||
return m.group(1).upper() if m else ""
|
||||
|
||||
# 查询结果行数上限
|
||||
_MAX_ROWS = 1000
|
||||
|
||||
@@ -156,10 +180,13 @@ async def execute_query(
|
||||
) -> QueryResponse:
|
||||
"""只读 SQL 执行。
|
||||
|
||||
安全措施:
|
||||
1. 拦截写操作关键词(INSERT / UPDATE / DELETE / DROP / TRUNCATE)
|
||||
2. 限制返回行数上限 1000 行
|
||||
3. 设置查询超时 30 秒
|
||||
安全措施(P0-8 修复 2026-05-04):
|
||||
1. 白名单:SQL 必须以 SELECT / WITH / EXPLAIN / SHOW 开头(去注释/空白后)
|
||||
2. 黑名单:深度防御,语句含 DML/DDL/DCL 关键词一律拒绝(覆盖 INSERT/UPDATE/DELETE/DROP/
|
||||
TRUNCATE/ALTER/CREATE/GRANT/REVOKE/COPY/CALL/COMMENT/VACUUM/REINDEX/CLUSTER/REFRESH/LOCK)
|
||||
3. 限制返回行数上限 1000 行
|
||||
4. 设置查询超时 30 秒
|
||||
5. 数据库连接为只读账号(get_etl_readonly_connection,默认 read-only 事务)
|
||||
"""
|
||||
sql = body.sql.strip()
|
||||
if not sql:
|
||||
@@ -168,11 +195,19 @@ async def execute_query(
|
||||
detail="SQL 语句不能为空",
|
||||
)
|
||||
|
||||
# 拦截写操作
|
||||
if _WRITE_KEYWORDS.search(sql):
|
||||
# 白名单:首关键词校验
|
||||
first_kw = _extract_first_keyword(sql)
|
||||
if first_kw not in _ALLOWED_PREFIXES:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="只允许只读查询,禁止 INSERT / UPDATE / DELETE / DROP / TRUNCATE 操作",
|
||||
detail=f"只允许只读查询,SQL 必须以 {' / '.join(_ALLOWED_PREFIXES)} 开头",
|
||||
)
|
||||
|
||||
# 黑名单:深度防御(剥离注释后再匹配,避免注释中"comment"等英文词误伤)
|
||||
if _DENY_KEYWORDS.search(_strip_comments(sql)):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="检测到禁止的关键词(DML/DDL/DCL),只允许只读查询",
|
||||
)
|
||||
|
||||
conn = get_etl_readonly_connection(user.site_id)
|
||||
|
||||
Reference in New Issue
Block a user