mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-16 05:45:35 +00:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user