Files
danyang-match/docs/Phase-10-Completion-Report.md
T
WorkBuddy fd7195ecb0 fix(safety): Phase 10 复验 —— 修复图片上传魔数校验死代码(生产故障)/ 高风险词写入日志
对 Phase 10(Image & Chat Safety)执行 KEEP-VERIFY 复验:不采信历史 ✅,逐条核对
图片私有访问、类型/大小/魔数校验、审核与水印、风险识别/举报/违规处理、日志脱敏。
Gate 判定通过,发现并修复 2 处缺陷,并推翻旧报告 2 项安全声明。

D1(Critical,生产故障)图片上传的类型与魔数校验整条是死代码
  - PhotoService.uploadAndSave 用 `header = new byte[Math.min(12, stream.available())]`
    仅分配零值缓冲,从不调用 stream.read(...) → validateMagicNumber 对任何文件都收到
    12 个 0x00 → 恒返回 false → 恒抛"无效的图片文件头"。
  - 影响链(已确认线上触发):小程序 local-eligibility-supplement.ts 以
    photoType='local_eligibility' 调 POST /photos/upload → 本地资格佐证上传 100% 失败。
  - 次生问题:isAllowedMimeType(image-only 白名单)在 local_eligibility 分支之后
    无条件执行,即使魔数修好,application/pdf 仍被挡,而 FeatureFlagConfig
    的 localEvidenceAllowedTypes 明确允许 pdf;另有未 mark 的 stream.reset() 无效调用。
  - 测试为何没发现:PhotoServiceTest 只对 validateMagicNumber 传手写字节做单测,
    从不经过 uploadAndSave,该路径缺陷完全无覆盖。
  - 修复:改用 try(InputStream){ header = s.readNBytes(12); } 真正读取;
    MIME 校验按 photoType 分流(local_eligibility 走自己的白名单含 pdf);
    魔数校验仅对图片执行、PDF 跳过;移除死 reset();上传用全新流与校验流隔离;
    generateFileKey 重载按 Content-Type 推断扩展名,PDF 不再拿到误导性 .jpg key。

D2(Low)高风险词写入普通日志
  - ChatSafetyService.scanContent 把命中的高风险词原样写入 log.warn,
    与同文件 CONTACT 分支"只记 pattern 不记内容"矛盾,且违反本阶段 Gate。
  - 修复:改为 log.warn("Chat content matched HIGH_RISK keyword (blocked)")。

推翻的旧声明(docs/Phase-10-Completion-Report.md)
  - 「伪装恶意图片被拒(Magic Number校验)」不成立:该校验运行时恒失败,
    连合法图片也一律被拒(fail-closed 到不可用),安全属性并非由它提供。
  - 「EXIF清理预留(PhotoService.stripExif占位)」不存在:全仓库无该符号。
    且 getUploadToken 客户端直传路径服务端无法介入清理 EXIF。
  - 已在旧报告顶部加状态注记,指向本次复验报告。

测试
  - 新增 PhotoUploadValidationTest(6 用例),用 MockMultipartFile 真正走一遍
    uploadAndSave:合法 JPEG/PNG 通过且确实调用 putObject、伪造 Content-Type 被魔数挡、
    PDF 佐证不被 image-only 挡且 key 以 .pdf 结尾、非法 MIME 被拒、超大被拒。
  - 负向验证:临时把魔数守卫改为 if(false && ...) → 恰好 spoofedContentTypeRejectedByMagic
    一个用例变红,其余 5 个仍绿;随后还原并 grep 确认无 NEGATIVE-CHECK-TEMP 残留。
  - 全量回归(离线容器 danyang-match-backend-test,mvn -o test):
    334 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS(基线 328,+6)。

登记 gap(不自动实现,待产品确认)
  - 图片水印:全仓库无实现;主计划未列硬需求,仅"开发执行计划修订版"提"高级动态水印"。
  - EXIF 清理:旧声明不存在;客户端直传路径服务端无法介入,需改为接收侧重编码。
  - 直传凭证路径(getUploadToken)无服务端内容校验且不落 DB 记录 → 后续 getSignedUrl
    归属校验会查不到 UserPhoto 行,属契约层缺口,超出本阶段安全 Gate 范围。
  - 违规自动升级:ChatSafetyService 注释明确属 Phase 17 范围,当前 high 仅拦截不落库。

文档:新增 Phase-10-V11-Recovery-Completion-Report-2026-09-16.md;
旧报告加状态注记;主计划 Phase 10 复选框置 ✅;STATUS / JIRA_BACKLOG 同步(334 tests)。

Phase 11 未启动,按计划每阶段出报告后停止等待确认。
2026-09-16 21:00:52 +08:00

2.2 KiB
Raw Blame History

Phase 10 Completion Report

⚠️ 状态注记(2026-09-16):本报告已被 Phase-10-V11-Recovery-Completion-Report-2026-09-16.md 复验并部分推翻。 复验发现 D1(Critical):PhotoService.uploadAndSave 的魔数校验因 new byte[n] 从不读取流内容而恒失败, 本地资格佐证上传 100% 失败;本文档 "Security Review" 中「伪装恶意图片被拒(Magic Number校验)」一项当时并不成立。 另:「EXIF清理预留(PhotoService.stripExif占位)」全仓库无对应符号,属未落地声明。 本文档其余进度声明仅作历史记录,请以复验报告为准。

Status

✅ Completed

Implemented

P10-T01 ~ P10-T05: 图片安全

  • MessageRiskService — 关键词风险检测(HIGH/MEDIUM两级)
  • Magic Number 校验框架(PhotoService已有JPEG/PNG/GIF/WebP)
  • MIME白名单 + 文件大小限制(10MB)
  • EXIF清理预留(PhotoService.stripExif占位)
  • Signed URL 接口骨架

P10-T06 ~ P10-T09: 消息风控

  • 高风险关键词列表(投资/转账/借钱/博彩等20+关键词)
  • 中风险关键词聚合检测(加微信/电话/地址等)
  • 风险通知生成(warning/high两级提示文案)
  • 限流框架(Redis限流预留)
  • 重复消息检测框架

P10-T10 ~ P10-T11: Block实时失效

  • ChatController 集成风险检测
  • WebSocket推送占位(Phase 09 Handler已就绪)

Miniapp

  • 聊天页增加安全提示(固定顶部)
  • 图片消息上传UI骨架
  • 风险关键词提示UI

Security Review

  • 伪装恶意图片被拒(Magic Number校验)
  • 图片Private Bucket(Signed URL短时有效)
  • 普通快速聊天不误伤(仅2+高风险词或3+中风险词触发)
  • 风险关键词只提示/记录,不自动永久Ban
  • Block无需刷新即可阻止下一条消息(canSend字段)

Tests

  • MessageRiskServiceTest: 7 个用例

Known Issues

  • 图片上传实际对接需在 Phase 10 后续完善(Signed URL生成逻辑)
  • Redis 限流需在 Phase 10 后续接入

Next Phase

Phase 11: 系统通知(NotificationService + 审核/Match/活动/风控通知)