fix(backend): 安全默认值、异常语义、代码生成注入面与定时任务内置处理器

本批为 t10 → t13 → t15 三轮实现 + 一轮复核驳回整改的最终形态(pytest 83 passed / ruff clean,
已部署并线上验证)。

异常语义
- CustomException 默认状态码 500 → 400(约 433 处裸调用不再被计为服务端故障),
  且「包装底层异常」的 130 处显式恢复 500;get_or_404 → 404;响应体结构不变

代码生成注入面(复核 blocker F1,二次修复)
- column_type 不再做字符串拼接:改为常量白名单派生(类型名 ∈ DB_TO_SQLALCHEMY ∪ {Boolean},
  长度仅取数字/正则捕获),彻底移除 split("(")[1]
- 列类型加形状白名单;字符串/注释/文档字符串用 \uXXXX,标识符过 safe_ident(含 Python/JS 关键字),
  .vue 文本与静态属性改 HTML 实体
- 落盘闸门由 ast.parse 升级为 assert_python_structure(语法 + 顶层语句白名单 + 与哨兵骨架逐节点比对)
  与 assert_text_structure(.vue/.ts/.toml 行结构);U+2028/2029 与 C1 控制符纳入转义

安全默认值
- 生产 CORS 不再回落 ["*"](未配置即拒绝跨域,可选 CORS_STRICT_STARTUP 直接启动失败)
- OAUTH_ALLOWED_HOSTS 不再默认 "*";新增 ALLOW_LOCALHOST_HOSTS 控制 Host 校验范围
- ALLOWED_HOSTS 收窄为按配置派生;健康检查暴露 code_exec_enabled / cors_origins_configured

定时任务
- SCHEDULER_ALLOW_CODE_EXEC 默认 False;新增 builtin:<模块名>[.<函数名>] 内置处理器路线(不经 exec)
- 判定收敛到 describe_schedulability;列表返回 schedulable / unschedulable_reason;
  启动 reconcile 与状态查询对「已启用但不可调度」打 WARN(去重、恢复后清除)
- 加固:内置处理器必须在本模块内定义或经 HANDLERS 声明,拒绝模块内导入的外部对象(复核 R1)
- 顺带修复:_WARNED_UNSCHEDULABLE 的 set.pop TypeError、job.next_run_time 缺失导致的列表 500

测试:新增 tests/test_generator_template_safety.py、test_security_defaults.py、
test_scheduler_schedulability.py(含恶意载荷、闸门红绿、静态护栏)
This commit is contained in:
zhangtao
2026-10-04 05:12:32 +08:00
parent 007864051a
commit 07fa789d2e
39 changed files with 3031 additions and 265 deletions
+71 -8
View File
@@ -4,6 +4,7 @@ from pathlib import Path
from typing import Any, Literal
from urllib.parse import quote_plus
from pydantic import model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from app.common.enums import EnvironmentEnum
@@ -54,7 +55,8 @@ class Settings(BaseSettings):
# ================================================= #
# ******************** 跨域配置 ******************** #
# ================================================= #
PROD_CORS_ORIGINS: str = "" # 生产环境允许的域名列表,逗号分隔,如 "https://admin.example.com,https://www.example.com"
PROD_CORS_ORIGINS: str = "" # 生产环境允许的域名列表,逗号分隔,如 "https://admin.example.com,https://www.example.com";生产留空=拒绝全部跨域请求
CORS_STRICT_STARTUP: bool = False # True 时「生产未配置 PROD_CORS_ORIGINS」直接启动失败(默认只告警,保持向后兼容)
ALLOW_METHODS: list[str] = ["*"] # 允许的HTTP方法
ALLOW_HEADERS: list[str] = ["*"] # 允许的请求头
ALLOW_CREDENTIALS: bool = True # 是否允许携带cookie
@@ -124,7 +126,7 @@ class Settings(BaseSettings):
# ================================================= #
# ******************* 任务调度配置 ****************** #
# ================================================= #
SCHEDULER_ALLOW_CODE_EXEC: bool = True # 是否允许定时任务执行用户提交的代码块(exec)。等同远程代码执行能力,生产环境强烈建议设为 False
SCHEDULER_ALLOW_CODE_EXEC: bool = False # 是否允许定时任务执行用户提交的代码块(exec)。等同远程代码执行能力,默认关闭,仅在显式配置为 true 时启用
# ================================================= #
# ******************* 口令策略配置 ****************** #
@@ -147,7 +149,9 @@ class Settings(BaseSettings):
OAUTH_QQ_APP_ID: str = ""
OAUTH_QQ_APP_SECRET: str = ""
OAUTH_STATE_TTL: int = 600 # OAuth state 参数过期时间(秒)
OAUTH_ALLOWED_HOSTS: list[str] = ["*"]
# 生产回跳域名白名单(**不再是通配符 "*"**):OAuth 回调域名必须命中其中之一。
# 非生产环境通常用 localhost/容器域名登录,故默认一并放行本机与回环地址;生产可按需收敛。
OAUTH_ALLOWED_HOSTS: list[str] = ["service.fastapiadmin.com", "*.fastapiadmin.com", "localhost", "127.0.0.1", "[::1]"]
# ================================================= #
# *************** 微信小程序配置(可选)************** #
@@ -182,7 +186,11 @@ class Settings(BaseSettings):
# ================================================= #
# ******************* 安全中间件配置 ****************** #
# ================================================= #
ALLOWED_HOSTS: list[str] = ["service.fastapiadmin.com", "*.fastapiadmin.com"] # 允许访问的主机名列表
ALLOWED_HOSTS: list[str] = ["service.fastapiadmin.com", "*.fastapiadmin.com"] # 允许访问的主机名列表(生产域名)
# 容器内健康检查走 http://localhost:8001、本地直连调试都需要 Host: localhost/127.0.0.1/[::1];
# 该放行范围由下面的开关控制——生产若不需要(例如健康检查改走 127.0.0.1 之外的内部域名)可置 False。
ALLOW_LOCALHOST_HOSTS: bool = True
LOCALHOST_HOSTS: list[str] = ["localhost", "127.0.0.1", "[::1]"] # ALLOW_LOCALHOST_HOSTS=True 时自动并入 ALLOWED_HOSTS
# 接口白名单(无需认证即可访问的接口路径,支持 * 开头表示前缀匹配)
WHITE_API_LIST_PATH: list[str] = [
@@ -236,15 +244,70 @@ class Settings(BaseSettings):
OPENAI_MODEL: str = ""
OPENAI_BASE_URL: str = "" # API Base URL,如 https://api.minimax.chat/v1
@model_validator(mode="after")
def _merge_localhost_hosts(self) -> "Settings":
"""按开关把 localhost/回环地址并入 ALLOWED_HOSTS。
健康检查与本地联调需要 ``Host: localhost``;把它们做成可配置项(而不是无条件写死),
生产可以用 ``ALLOW_LOCALHOST_HOSTS=False`` 收敛 Host 头校验范围。
返回:
- Settings: 自身(pydantic after-validator 约定)。
"""
if self.ALLOW_LOCALHOST_HOSTS:
for host in self.LOCALHOST_HOSTS:
if host not in self.ALLOWED_HOSTS:
self.ALLOWED_HOSTS.append(host)
return self
# ================================================= #
# ******************* 动态配置 ******************* #
# ================================================= #
@property
def ALLOW_ORIGINS(self) -> list[str]:
"""根据环境动态返回 CORS 允许的域名列表。"""
if self.ENVIRONMENT == EnvironmentEnum.PROD and self.PROD_CORS_ORIGINS:
return [origin.strip() for origin in self.PROD_CORS_ORIGINS.split(",") if origin.strip()]
return ["*"]
"""根据环境动态返回 CORS 允许的域名列表。
安全默认(见 backend/audit-security.md S9):
- 非生产环境:返回 ``["*"]``,保留本地联调便利;
- 生产环境:只接受**显式配置**的域名清单。未配置、或清单里只有通配符 ``"*"`` 时返回空列表
(空列表 = 不下发任何跨域许可响应头;同源部署的 Web 端不受影响),并打 warning 提示运维配置。
历史行为是"未配置就回落 ``["*"]``",叠加 ``ALLOW_CREDENTIALS=True`` 等价于允许任意站点
带凭据跨域访问。
"""
if self.ENVIRONMENT != EnvironmentEnum.PROD:
return ["*"]
origins = [origin.strip() for origin in (self.PROD_CORS_ORIGINS or "").split(",") if origin.strip()]
if "*" in origins:
origins = [origin for origin in origins if origin != "*"]
self._warn_insecure_cors(
"生产环境 CORS 不允许通配符 '*'(与 ALLOW_CREDENTIALS=True 组合会放行任意站点带凭据跨域),已忽略该值;请显式配置 PROD_CORS_ORIGINS",
)
if not origins:
message = "生产环境未显式配置 PROD_CORS_ORIGINS,已按安全默认拒绝全部跨域请求(同源部署不受影响);如需跨域请在 .env.prod 中配置 PROD_CORS_ORIGINS"
if self.CORS_STRICT_STARTUP:
# 严格模式:宁可启动失败,也不要带着"跨域被全量拒绝"的隐性配置上线
raise RuntimeError(f"CORS_STRICT_STARTUP=True 且 {message}")
self._warn_insecure_cors(message)
return origins
@staticmethod
def _warn_insecure_cors(message: str) -> None:
"""输出安全告警(日志不可用时静默,绝不影响配置读取)。
参数:
- message (str): 告警内容。
返回:
- None
"""
# 延迟导入:app.core.logger 会反向 import 本模块,模块级导入会形成循环依赖
try:
from app.core.logger import logger
logger.warning(message)
except Exception: # noqa: BLE001 - 告警失败不得影响配置读取
pass
# ================================================= #
@property