# Changelog — Settings 安全加固 + UX 迭代

**日期**: 2026-05-30  
**Commit**: a50f179  
**类型**: 安全修复 + UX 改进  
**涉及文件**:
- `server/api/settings.py` — SSRF 防护 + key 白名单 + 值校验 + IP 审计
- `server/api/auth.py` — 密码修改要求 TOTP
- `server/application/services/auth_service.py` — _verify_totp staticmethod
- `web/app/settings.html` — UX 全面改进 + 密码修改重定向
- `web/app/login.html` — 密码修改后提示

## 安全修复 (5 项)

### S-01 SSRF 防护 (P0)
`POST /api/settings/ip-allowlist/parse-subscription` 可被利用探测内网。
- 新增 `_is_private_host()` 函数：IP 直检 + DNS 解析后二次校验
- 封禁：loopback/link-local/private/reserved IP 段
- `follow_redirects=False`，手动跟随最多 3 次重定向，每次检查目标
- 响应大小限制 1MB

### S-02 设置值校验 (P0)
`PUT /api/settings/{key}` 接受任意值。
- 新增 `MUTABLE_KEYS` 白名单（27 个已知 key），未知 key → 403
- 新增 `SETTING_VALIDATORS`：6 个 INT 设置项的类型+范围校验
  - db_pool_size/db_max_overflow: 1-1000
  - heartbeat_timeout: 10-600
  - cpu/mem/disk_alert_threshold: 1-100
- 非整数值 → 400 "必须是整数"，范围越界 → 400 "范围: X-Y"

### S-05 改密码后重登录 (P1)
密码修改成功后前端仅清空表单，用户留在 stale JWT 页面。
- 成功后跳转 `/app/login.html?msg=password_changed`
- login.html 显示绿色提示 "密码已修改，请重新登录"

### S-07 IP 操作审计 (P1)
`add_manual_ips` 和 `remove_allowlist_ip` 缺少审计日志。
- 两个端点添加 `request: Request` 参数
- 添加标准 AuditLog 写入（action/detail/ip_address）

### S-09 改密码要求 TOTP (P1)
已启用 TOTP 的管理员改密码只需密码，可绕过 TOTP 保护。
- `ChangePasswordRequest` 新增 `totp_code: Optional[str]`
- `change_password` handler 检查：TOTP 已启用 + 未提供 code → 400
- `AuthService._verify_totp` 改为 `@staticmethod` 供 auth.py 调用

## UX 改进 (5 项)

### UX-01 输入框样式+类型
- 密码输入框添加 `focus:outline-none focus:ring-2 focus:ring-brand`
- 设置项 input type 映射：数值→`type="number" min="1"`，URL→`type="url"`
- 所有动态渲染 input 统一添加 focus ring

### UX-02 改密码 loading 状态
- 按钮点击后 `disabled=true + textContent='提交中...'`
- 请求完成 `finally` 块恢复

### UX-03 加载失败 toast
- `loadSettings` / `loadTotpStatus` / `loadAllowlist` 的空 catch 改为 `console.error + toast`

### UX-04 保存失败回滚
- `saveSetting()` 失败时调用 `loadSettings()` 恢复原值

### UX-05 IP 格式校验
- 前端正则校验：IP (`x.x.x.x`) / CIDR (`x.x.x.x/n`) / 域名
- 后端 `IpAllowlistRequest` Pydantic `model_validator` 校验

## 验证结果

| 测试 | 结果 |
|------|------|
| SSRF localhost | ✅ 400 "不允许访问内网地址" |
| SSRF metadata | ✅ 400 "不允许访问内网地址" |
| 未知 key | ✅ 403 |
| INT 范围 99999 | ✅ 400 "范围: 1-1000" |
| INT 类型 "abc" | ✅ 400 "必须是整数" |
| 有效值 75 | ✅ 200 |
| IP 审计日志 | ✅ "添加 1 条手动 IP: 192.0.2.1" |
| TOTP schema | ✅ totp_code Optional 字段 |
| Input types | ✅ number/url/text 正确映射 |

## 是否需迁移/重启
- 需要重启后端服务（新增白名单+校验+SSRF 逻辑）
