Smail/docs/安全审查工程优化审核报告.md
smail f60d3b0880
Some checks are pending
Security Checks / security (push) Waiting to run
整理前后端与测试构建目录并完善配置和安全引导
统一 frontend、backend、test、test-log 和 out 目录,更新构建、部署及测试入口;纳入配置管理兼容与首次安全引导修复;排除本机配置、密钥和测试产物。
2026-09-05 19:19:52 +08:00

269 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Smail 工程优化审核报告
> 审核日期:2026-08-24
> 审核基线:`e91a641 重命名项目并补充插件与安全整改文档`
> 审核分支:`main`
> 审核范围:当前工作区 `D:\Desktop\Smail`,依据 `docs\安全审查风险整改任务清单.md` 对 SEC-001 至 SEC-022 进行工程优化与安全整改复核。
> 审核结论:本轮未发现可以标记为“复审通过”的任务;整改清单中的 22 项状态保持不变。
## 一、审核结论摘要
当前项目可以完成前端生产构建,部分后端单元包测试通过,但尚不具备安全上线条件。主要阻断原因如下:
1. SMTP 587 端口在 STARTTLS 建立前允许认证,并且认证日志会输出密码。
2. 初始化接口暴露在普通 HTTP 服务上,缺少一次性令牌、来源限制、速率限制和失败次数限制。
3. `HttpsEnabled == 2` 时,HTTP 直接承载完整业务路由和会话。
4. 密码认证仍使用固定盐 MD5;登录、初始化、改密、管理员重置和 SMTP 登录没有统一的新凭据验证服务。
5. 邮件移动、分组删除和 IMAP UID 查询存在用户归属校验缺口。
6. 请求体、邮件附件和 Base64 解码后的内容没有形成统一的大小限制策略。
7. 未形成统一 CSRF、防安全响应头、插件隔离、TLS 证书校验和依赖漏洞审计流程。
8. `go vet ./...` 仍然失败;全量 `go test ./...` 存在删除真实配置和数据库的测试副作用,未在当前工作区执行。
因此,建议先完成 P0 全部整改,再进入 P1 的认证、租户隔离和输入安全整改,最后完成 P2 的工程加固和 CI 门禁。
## 二、复审状态总览
| 任务编号 | 优先级 | 审核状态 | 结论 | 关键证据 |
| --- | --- | --- | --- | --- |
| SEC-001 | P0 | 未通过 | 不通过 | `backend/listen/smtp_server/smtp.go:28` 在 587 端口设置 `AllowInsecureAuth = true` |
| SEC-002 | P0 | 未通过 | 不通过 | `backend/listen/smtp_server/action.go:68,92,96` 记录用户名、密码和完整用户对象 |
| SEC-003 | P0 | 未通过 | 不通过 | `setup_server.go:27-29,52-58` 暴露初始化服务;业务拦截只按 `/api/setup` 放行 |
| SEC-004 | P0 | 未通过 | 不通过 | `http_server.go:69-90` 在关闭 HTTPS 时仍注册业务路由和会话 |
| SEC-005 | P1 | 未通过 | 不通过 | `group.go:123-126` 只校验邮件所属用户,未校验目标分组所属用户 |
| SEC-006 | P1 | 未通过 | 不通过 | `group.go:88` 更新 `user_email` 时缺少 `user_id` 条件 |
| SEC-007 | P1 | 未通过 | 不通过 | `utils/password/encode.go` 使用两次 MD5 和固定字符串盐 |
| SEC-008 | P1 | 未通过 | 不通过 | 多个控制器直接 `io.ReadAll(req.Body)`;附件缺少统一数量、总量和解码后大小限制 |
| SEC-009 | P1 | 未通过 | 不通过 | 路由使用 `mux.HandleFunc`,未见统一 HTTP 方法和 CSRF 防护中间件 |
| SEC-010 | P1 | 未通过 | 不通过 | 邮件清理允许全局 `style`、`<style>` 和远程图片;前端使用 `v-html` |
| SEC-011 | P1 | 未通过 | 不通过 | 缺少统一安全响应头;插件 iframe 未设置 `sandbox` |
| SEC-012 | P1 | 未通过 | 不通过 | SSL 初始化仅 `os.Stat` 用户路径并直接写入配置,未做受控目录和匹配校验 |
| SEC-013 | P1 | 未通过 | 不通过 | `utils/smtp/smtp.go:80` 设置 `InsecureSkipVerify = true` |
| SEC-014 | P1 | 未通过 | 不通过 | `backend/main_test.go:35-36` 删除真实配置和数据库 |
| SEC-015 | P2 | 未通过 | 不通过 | 配置、证书和公钥文件使用 `0666`,配置目录使用 `0755` |
| SEC-016 | P2 | 未通过 | 不通过 | `services/auth/auth.go:45` 生成 1024 位 RSA DKIM 密钥 |
| SEC-017 | P2 | 未通过 | 不通过 | 附件下载使用错误的 `ContentType` 响应头,文件名直接拼接到响应头 |
| SEC-018 | P2 | 未通过 | 不通过 | `config.go:264-273` 使用字符串判断绝对路径,未使用平台无关路径边界校验 |
| SEC-019 | P2 | 原始审查不通过,复审通过 | 通过(生产模式失败关闭) | 已完成插件白名单、最小环境、Linux 独立用户、chroot 临时根目录、网络命名空间和资源限制;Windows 等不具备隔离能力的平台生产模式拒绝插件启动 |
| SEC-020 | P2 | 未通过 | 不通过 | `detail.go:79` UID 查询只按 `id`,未加入当前用户条件 |
| SEC-021 | P2 | 未通过 | 不通过 | 未安装 `govulncheck`;前端只有 `yarn.lock`,没有统一锁文件和 CI 扫描门禁 |
| SEC-022 | P2 | 未通过 | 不通过 | `go vet ./...` 报告 7 处未命名结构体字段问题,CI 未执行 vet |
## 三、逐项工程审核
### 1. P0 上线阻断项
#### SEC-001:SMTP 587 在 STARTTLS 前认证
**审核结果:不通过。**
`backend/listen/smtp_server/smtp.go:28` 将 587 端口的 `AllowInsecureAuth` 设置为 `true`。当前配置会使 SMTP 服务器可能在 TLS 升级前声明并接受认证能力,与任务清单要求的“仅 STARTTLS 成功后认证”不一致。
**优化要求:**
- 587 端口必须在 TLS 建立前不返回 `AUTH` 能力。
- STARTTLS 失败、未执行 STARTTLS 或连接降级时必须拒绝认证。
- 465 端口保持隐式 TLS,并与 587 使用不同的启动方式。
- 增加 SMTP 协议级回归测试,验证 STARTTLS 前后 EHLO 能力和认证结果。
#### SEC-002:SMTP 日志泄漏凭据
**审核结果:不通过。**
`backend/listen/smtp_server/action.go:68` 使用日志输出用户名和明文密码;`action.go:92` 在认证成功时输出完整用户对象,`action.go:96` 在认证失败时再次输出用户名和密码。该问题属于凭据直接泄漏,必须优先处理。
**优化要求:**
- 日志中不得出现密码、密码哈希、完整用户对象或第三方认证密钥。
- 认证日志只保留请求 ID、脱敏账号、结果、来源地址和失败原因分类。
- 为认证失败日志增加敏感字段回归测试或日志审计测试。
#### SEC-003:初始化接口保护不足
**审核结果:不通过。**
`backend/listen/http_server/setup_server.go:27-29` 将初始化 API 挂载到初始化 HTTP 服务,`setup_server.go:52-58` 直接监听端口。`backend/listen/http_server/https_server.go:135-137` 在未初始化时仅按路径 `/api/setup` 放行,没有发现一次性初始化令牌、失败次数限制、速率限制或受控来源校验。
**优化要求:**
- 初始化服务默认只允许本机或显式配置的受控来源访问。
- 使用一次性、短时效的初始化令牌,并在初始化成功后立即失效。
- 对初始化失败进行速率限制和失败次数限制。
- 初始化完成后,初始化 API 必须永久返回拒绝,不得依赖前端隐藏入口。
#### SEC-004:明文 HTTP 承载登录会话
**审核结果:不通过。**
`backend/listen/http_server/http_server.go:69-90` 在 `HttpsEnabled == 2` 时直接注册完整业务路由,并通过 `session.Instance.LoadAndSave(mux)` 启用会话。当前未见统一的 HTTPS 强制策略、Cookie `Secure`、`HttpOnly`、`SameSite`、HSTS 以及安全响应头配置。
**优化要求:**
- 生产模式禁止使用明文 HTTP 承载登录和业务会话。
- HTTP 仅允许健康检查、ACME 挑战或重定向到 HTTPS。
- 会话 Cookie 必须设置 `Secure`、`HttpOnly` 和合理的 `SameSite`。
- 在 TLS 终止于反向代理时明确可信代理边界,并验证转发协议头。
### 2. P1 高优先级整改项
#### SEC-005:邮件移动目标分组归属
**审核结果:不通过。**
`backend/services/group/group.go:123-126` 的更新语句只通过 `email_id` 和 `user_id` 限制被更新的邮件,但直接使用请求传入的 `groupId`,没有先确认目标分组属于 `ctx.UserID`。攻击者可以尝试引用其他用户的分组 ID。
**优化要求:**先查询并锁定目标分组的 `id + user_id`,不存在或不属于当前用户时拒绝操作;再在同一事务内更新邮件,并增加双用户隔离回归测试。
#### SEC-006:删除分组后的邮件更新
**审核结果:不通过。**
`backend/services/group/group.go:88` 的 `update user_email set group_id=0 where group_id in (...)` 没有附加 `user_id` 条件,存在跨用户修改邮件分组的风险。分组统计查询也存在按 `group_id` 查询而未统一附带用户条件的路径。
**优化要求:**删除分组、清理邮件和统计查询均必须同时带当前用户条件;删除和清理操作必须在同一事务中完成,并增加失败回滚测试。
#### SEC-007:统一密码哈希与凭据验证
**审核结果:不通过,且与既定整改原则直接冲突。**
`backend/utils/password/encode.go` 使用两次 MD5 和固定字符串盐。当前登录、初始化、改密、管理员重置和 SMTP 登录仍使用旧 `password.Encode` 路径;HTTP 临时令牌也依赖 MD5 计算。项目没有 Argon2id、统一 `PasswordService` 或 `CredentialValidator`。
**优化要求:**
- 删除旧算法运行时兼容路径,不保留 MD5 与新算法双轨验证。
- 使用 Argon2id,并将算法参数、盐和版本信息保存在密码哈希值中。
- 登录、初始化、改密、管理员重置和 SMTP 登录统一调用同一凭据验证服务。
- 第三方配置验证所需的可逆密钥不得使用密码哈希保存,应使用受控加密存储;日志不得打印明文。
- 既有账号统一通过密码重置或重新初始化设置新密码,不在登录时自动兼容旧 MD5。
#### SEC-008:请求体与附件大小限制
**审核结果:不通过。**
登录、初始化、用户、设置、规则、分组、邮件和插件接口存在直接 `io.ReadAll(req.Body)` 的读取方式。邮件附件 Base64 解码未形成单附件、总附件数、总请求体、解码后大小和超时的统一限制,存在内存消耗风险。
**优化要求:**在统一中间件使用 `http.MaxBytesReader` 或等价机制,按接口分类设置上限;附件在解码前后都校验大小,限制数量、总量和 MIME 类型,并对超限返回明确错误。
#### SEC-009:CSRF 与 HTTP 方法限制
**审核结果:不通过。**
路由统一使用 `mux.HandleFunc`,未发现状态修改接口的统一方法限制、CSRF Token、`Origin` 或 `Referer` 校验中间件。Cookie 会话下的删除、移动、发信、改密、用户和规则操作需要统一保护。
**优化要求:**状态修改接口拒绝 GET,采用 CSRF Token 与可信来源校验组合;登录、初始化等特殊流程单独定义认证边界,并增加跨站请求回归测试。
#### SEC-010:邮件 HTML 清理与展示隔离
**审核结果:不通过。**
`backend/dto/parsemail/email.go:122,129,132,140-141` 允许全局 `style`、`img` 的 `style`、`a` 的 `style` 及 `<style>` 元素,并允许远程图片。`frontend/src/views/EmailDetailView.vue:52` 使用 `v-html` 将邮件 HTML 直接插入主页面 DOM。虽然已有 bluemonday 和 XSS 测试基础,但当前策略仍过宽,且缺少邮件展示专用隔离环境。
**优化要求:**严格收紧 HTML 白名单,默认禁止脚本、事件属性、危险 URL、远程资源和 CSS 行为;优先使用邮件专用受限 iframe,配合 `sandbox` 和邮件专用 CSP;补充 CSS、URL、SVG、表单和重定向测试。
#### SEC-011:安全响应头与插件 iframe
**审核结果:不通过。**
当前 HTTP 拦截器主要设置 `Content-Type`,未发现统一设置 CSP、`X-Content-Type-Options`、`Referrer-Policy`、`Permissions-Policy` 等安全响应头。`frontend/src/components/PluginSettings.vue:12` 的 iframe 没有 `sandbox`,也没有明确的来源、权限和通信边界。
**优化要求:**建立统一安全响应头中间件;插件 iframe 使用最小权限 `sandbox`,限制来源和可用能力,避免访问主应用 Cookie、DOM 和敏感 API。
#### SEC-012:初始化 SSL 路径与证书私钥校验
**审核结果:不通过。**
`backend/controllers/setup.go:148-162` 只使用 `os.Stat` 检查用户提交的证书和私钥路径,`backend/services/setup/ssl/ssl.go:54-77` 直接把路径写入配置。未发现受控目录、路径穿越、符号链接逃逸、文件类型和证书私钥匹配校验。
**优化要求:**使用受控证书目录和 `filepath.Clean`,拒绝目录、符号链接逃逸和工作区外路径;解析证书与私钥并验证公钥匹配,错误信息不得泄漏敏感路径内容。
#### SEC-013:出站 SMTPS 证书校验
**审核结果:不通过。**
`backend/utils/smtp/smtp.go:79-82` 设置 `InsecureSkipVerify: true`,出站 SMTPS 默认跳过证书链和主机名校验,容易遭受中间人攻击。
**优化要求:**默认启用证书验证和主机名校验;如确有企业自签证书场景,使用显式受控 CA 配置,不提供无条件跳过校验的默认路径。
#### SEC-014:测试副作用
**审核结果:不通过,且本轮未执行全量测试。**
`backend/main_test.go:35-36` 在 `TestMain` 中直接删除 `config/config.json` 和 `config/smail_temp.db`,并以当前工作目录启动主服务。为遵守“未经同意不删除任何文件”的约束,本轮没有执行 `go test ./...`。
**优化要求:**测试使用临时目录、测试专用配置和测试端口;通过依赖注入或测试启动器隔离数据库和配置。整改后在隔离环境执行 `go test ./...`,并验证测试前后工作区文件校验和不变。
### 3. P2 加固与质量项
| 任务 | 审核结论与证据 | 优化方向 |
| --- | --- | --- |
| SEC-015 | 不通过:`config.go:215,226,242` 和 `services/setup/ssl/ssl.go:141-151,225-235` 使用 `0666` 写入敏感文件,目录使用 `0755`。 | 私钥使用 `0600`,配置按最小权限创建,目录权限按运行账户和平台策略收紧。 |
| SEC-016 | 不通过:`services/auth/auth.go:45` 使用 `rsa.GenerateKey(rand.Reader, 1024)`。 | 改为至少 2048 位,并增加密钥长度回归测试和旧密钥轮换策略。 |
| SEC-017 | 不通过:`controllers/attachments.go:47-48` 使用错误的 `ContentType` 头名称,文件名直接拼接 `Content-Disposition`,未设置 `nosniff`。 | 使用标准响应头、RFC 兼容文件名编码、文件名清理、`X-Content-Type-Options: nosniff`,并补充 CRLF 和路径字符测试。 |
| SEC-018 | 不通过:`config.go:264-273` 通过是否以 `/` 开头判断绝对路径。 | 使用 `filepath.IsAbs`、`filepath.Clean` 和目录边界校验,覆盖 Windows 盘符、UNC、相对路径和符号链接场景。 |
| SEC-019 | 原始问题已整改:生产模式仅启动可建立隔离的插件,不满足隔离条件时失败关闭。 | 已实现白名单、最小环境、Linux 独立用户、chroot、网络隔离和资源限制;Windows 生产模式拒绝不可隔离插件。 |
| SEC-020 | 不通过:`services/detail/detail.go:76-80` 的 UID 分支按 `id` 查询,缺少 `user_id = ctx.UserID`。 | 所有 UID、邮件 ID、附件 ID 查询统一绑定用户条件,增加跨用户 IMAP 回归测试。 |
| SEC-021 | 不通过:当前环境未安装 `govulncheck` 和 `yarn.cmd`;前端存在 `yarn.lock` 但没有 `package-lock.json`,发布工作流只构建不扫描。 | 明确单一前端包管理器和锁文件,CI 执行 Go 与前端依赖扫描,并设置发布阻断或安全例外审批。 |
| SEC-022 | 不通过:`go vet ./...` 报告 `email.go` 4 处、`controllers/group.go` 3 处未命名结构体字段;发布工作流未执行 `go vet`。 | 修复 keyed struct literal,并将 `go vet ./...` 设为合并前门禁。 |
## 四、已执行验证
| 验证项 | 命令 | 结果 |
| --- | --- | --- |
| 前端构建 | `cd frontend; npm.cmd run build` | 通过;Vite 构建成功,但存在单个 JavaScript 包超过 500 kB 的性能警告,建议后续代码分包。 |
| 后端重点包测试 | `go test ./utils/password ./dto/parsemail ./utils/address ./utils/utf7 ./services/rule/match` | 通过。 |
| 静态检查 | `cd backend; go vet ./...` | 失败;发现 7 处未命名结构体字段问题。 |
| Go 全量测试 | `cd backend; go test ./...` | 未执行;`backend/main_test.go` 会删除真实配置和数据库,需先隔离测试环境。 |
| Go 依赖模块枚举 | `cd backend; go list -m all` | 可执行;仅证明模块可解析,不等同于漏洞扫描。 |
| Go 漏洞扫描 | `govulncheck ./...` | 未执行;当前环境未安装 `govulncheck`。 |
| 前端依赖审计 | `npm audit` 或 `yarn audit` | 未形成可复现流程;前端锁文件和包管理器尚未统一。 |
## 五、整改优先级与验收顺序
### 第一阶段:P0 上线阻断
1. SEC-001:修复 587 STARTTLS 前认证。
2. SEC-002:清理 SMTP 敏感日志。
3. SEC-003:保护初始化入口并设置一次性令牌。
4. SEC-004:禁止明文 HTTP 承载业务会话。
### 第二阶段:认证与隔离
1. SEC-007:以 Argon2id 和统一凭据服务替换旧 MD5 运行时路径,不保留旧算法兼容逻辑。
2. SEC-005、SEC-006、SEC-020:补齐邮件、分组和 IMAP UID 的用户归属条件。
3. SEC-008、SEC-009:统一请求限制、CSRF 防护和 HTTP 方法限制。
4. SEC-010、SEC-011:收紧邮件内容展示并隔离插件 iframe。
5. SEC-012、SEC-013:限制 SSL 文件路径并恢复出站 TLS 校验。
### 第三阶段:工程质量和持续安全
1. SEC-014:隔离全量测试,不再触碰真实配置和数据库。
2. SEC-022:修复 vet 并建立静态检查门禁。
3. SEC-015 至 SEC-019:完成权限、密钥、附件、路径和插件边界加固。
4. SEC-021:建立 Go、前端依赖漏洞扫描和发布阻断流程。
## 六、整改后的统一验收基线
整改完成后,必须至少通过以下验证:
- SMTP:587 在 STARTTLS 前不返回 `AUTH`,TLS 建立后才能认证;465 使用隐式 TLS。
- 认证:登录、初始化、改密、管理员重置、SMTP 登录统一通过凭据验证服务;不再执行 MD5 兼容校验。
- 第三方配置:需要向第三方发起连接的密钥使用受控加密存储,不使用不可逆密码哈希代替可逆密钥。
- 会话:HTTP 不承载业务会话;Cookie 具备 `Secure`、`HttpOnly`、`SameSite`;响应头和 HSTS 策略可验证。
- 初始化:令牌一次性、短时效、失败限速;初始化完成后接口失效。
- 隔离:双用户邮件、附件、分组、IMAP UID 交叉访问均被拒绝。
- 输入:请求体、附件数量、附件总量、解码后大小和处理超时均有上限。
- 内容:邮件 HTML 严格清理,远程资源策略明确,邮件与插件在隔离容器或受限 iframe 中展示。
- 测试:`go test ./...` 在临时目录执行,`go vet ./...` 成功,测试不会改变工作区真实文件。
- 依赖:Go 和前端锁文件、漏洞扫描结果、CI 运行记录可追溯到提交和构建产物。
## 七、审核边界和遗留风险
- 本报告是工程优化审核结果,不代表任何整改项已经完成,也不替代后续代码复审。
- 本轮没有修改业务源码,没有删除文件,没有修改整改清单中的任务状态,也没有提交或推送新的 Git 变更。
- 前端构建通过不代表 XSS、会话和插件隔离风险已解决。
- 重点包测试通过不代表整个服务、SMTP、初始化流程和跨用户边界已通过验证。
- 全量 Go 测试、Go 漏洞扫描和前端依赖漏洞扫描仍需在整改后、隔离环境中完成。
## 复审补充:SEC-019
2026-08-24 复审时,SEC-019 已按生产失败关闭原则完成整改。Linux 生产模式使用独立 UID/GID、chroot 临时根目录、默认无网络命名空间和资源限制;Windows 及其他平台无法建立同等隔离时拒绝生产插件启动。