Some checks are pending
Security Checks / security (push) Waiting to run
统一 frontend、backend、test、test-log 和 out 目录,更新构建、部署及测试入口;纳入配置管理兼容与首次安全引导修复;排除本机配置、密钥和测试产物。
269 lines
19 KiB
Markdown
269 lines
19 KiB
Markdown
# 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 及其他平台无法建立同等隔离时拒绝生产插件启动。
|