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

19 KiB
Raw Permalink Blame History

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 及其他平台无法建立同等隔离时拒绝生产插件启动。