统一 frontend、backend、test、test-log 和 out 目录,更新构建、部署及测试入口;纳入配置管理兼容与首次安全引导修复;排除本机配置、密钥和测试产物。
19 KiB
Smail 工程优化审核报告
审核日期:2026-08-24
审核基线:e91a641 重命名项目并补充插件与安全整改文档
审核分支:main
审核范围:当前工作区D:\Desktop\Smail,依据docs\安全审查风险整改任务清单.md对 SEC-001 至 SEC-022 进行工程优化与安全整改复核。
审核结论:本轮未发现可以标记为“复审通过”的任务;整改清单中的 22 项状态保持不变。
一、审核结论摘要
当前项目可以完成前端生产构建,部分后端单元包测试通过,但尚不具备安全上线条件。主要阻断原因如下:
- SMTP 587 端口在 STARTTLS 建立前允许认证,并且认证日志会输出密码。
- 初始化接口暴露在普通 HTTP 服务上,缺少一次性令牌、来源限制、速率限制和失败次数限制。
HttpsEnabled == 2时,HTTP 直接承载完整业务路由和会话。- 密码认证仍使用固定盐 MD5;登录、初始化、改密、管理员重置和 SMTP 登录没有统一的新凭据验证服务。
- 邮件移动、分组删除和 IMAP UID 查询存在用户归属校验缺口。
- 请求体、邮件附件和 Base64 解码后的内容没有形成统一的大小限制策略。
- 未形成统一 CSRF、防安全响应头、插件隔离、TLS 证书校验和依赖漏洞审计流程。
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 上线阻断
- SEC-001:修复 587 STARTTLS 前认证。
- SEC-002:清理 SMTP 敏感日志。
- SEC-003:保护初始化入口并设置一次性令牌。
- SEC-004:禁止明文 HTTP 承载业务会话。
第二阶段:认证与隔离
- SEC-007:以 Argon2id 和统一凭据服务替换旧 MD5 运行时路径,不保留旧算法兼容逻辑。
- SEC-005、SEC-006、SEC-020:补齐邮件、分组和 IMAP UID 的用户归属条件。
- SEC-008、SEC-009:统一请求限制、CSRF 防护和 HTTP 方法限制。
- SEC-010、SEC-011:收紧邮件内容展示并隔离插件 iframe。
- SEC-012、SEC-013:限制 SSL 文件路径并恢复出站 TLS 校验。
第三阶段:工程质量和持续安全
- SEC-014:隔离全量测试,不再触碰真实配置和数据库。
- SEC-022:修复 vet 并建立静态检查门禁。
- SEC-015 至 SEC-019:完成权限、密钥、附件、路径和插件边界加固。
- 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 及其他平台无法建立同等隔离时拒绝生产插件启动。