Protect upload cleanup and atomic completion paths, add durable media worker leases and brand asset validation, and introduce verified migration/bootstrap/deployment gates.
7.6 KiB
存储域名与品牌资源事故复盘(2026-08-11)
结论
生产 Header Logo 的旧地址不是 2026 年近期修复首次写入的,而是 2025 年遗留配置。近期系统设置安全重构虽然没有制造这条历史数据,但沿用了过期默认值,也没有识别“展示代理地址可能被管理页再次写回数据库”的问题,因此同类故障仍可能复发。
本次治理把存储引用分成两类:
LEGACY_READ_ONLY:只允许读取、代理或迁移历史素材。- 当前写入:默认值、新上传、恢复默认、系统设置和新数据库记录只能使用本地
/brand/...或当前s3.jiansun.vip/gongxiangpan/...。
2026-08-12 13:13(Asia/Shanghai)重新只读核查当前正式数据库,并与公开 /api/system-settings 结果逐字段比对:唯一设置行的 Header / Login Logo 已是 https://s3.jiansun.vip/gongxiangpan/... 当前可信桶地址,用户指出的旧精确 Header URL 已不在现有行中;两个公网对象均返回 HTTP 200、image/png,浏览器实际解码为 800×800,并在正式首页显示为 40×40 图标。因此“现有行仍保存旧地址”已不再是当前事实。
但这不等于数据库治理已经完成:同一只读核查显示 headerLogoUrl / loginLogoUrl 的数据库列默认值仍是退役 http://s3.1080p.hk/...,本轮三条目标 migration 也尚无已完成记录。仓库 migration 会在已有 system_settings 历史基线上补齐列、修正未来默认值,并且只把品牌两列中匹配退役前缀的值改为本地 /brand/...;当前可信桶中的合法自定义 Logo 不会被覆盖。仓库更早的历史 migration 本身不能从空数据库完整重放,本轮没有把这件事包装成“全新建库已修复”。
误导来源
- Prisma 的 Logo 默认值仍指向旧域名。
- 系统设置行自动创建时仍使用旧默认值。
- 后台“恢复默认”按钮会重新保存旧地址。
- 管理员 GET 返回展示代理 URL,页面保存其它设置时可能把代理 URL 当原值写回。
- favicon 上游失败后返回透明 1×1 图片,接口看似成功但用户看不到 Logo。
- Header、登录、注册、完善资料页没有稳定的本地图片回退。
- 在线历史恢复接口会把历史桶 URL 直接写入新素材。
- 旧项目记忆把已经下线的资源域名描述为新素材正式入口。
- 现有存储迁移文档只核对素材预览,没有把 Header、登录 Logo、favicon、后台默认值和 Schema 默认值列入验收。
- Git 历史中曾存在包含旧生产事实与敏感配置的报告;工作树删除不能消除历史风险,需单独纳入秘密扫描和凭据风险登记,不能在当前修复中贸然改写远端历史。
Git 时间线证据
3fff87c(2025-10-27)引入 Logo 分离上传;生产旧 Header 文件名中的时间戳也落在这一天。2b7cd7f(2025-12-27)的历史数据库报告已记录用户本次指出的精确 Header URL,证明它不是 2026-07 的安全修复首次写入。3cba791/7c913b6(2026-07-30/31)的系统设置脱敏修复没有主动更新生产 Logo,但仍复制了旧 Schema 默认值、API 默认值和后台重置值。这是本轮必须承认的遗漏:修复了凭据泄漏,却没有同时清理品牌资源的旧写入路径。d69eb6e(2026-08-07)只处理视频手动预览图,不涉及 Logo,也没有解决视频队列跨部署恢复。
因此,事故来源不是“某一次新提交把当前生产行直接改成旧 Header”,而是历史脏值长期存在,加上后续修复没有铲除所有复发入口。两部分都必须处理,不能只修数据库或只加页面回退。
| 文件 / 资料 | 造成的错误判断或复发风险 | 本次处理 |
|---|---|---|
prisma/schema.prisma |
新建设置行时可能自动填入旧 Logo | 改为本地品牌默认值并新增 migration |
app/api/system-settings/route.ts |
设置不存在时写旧默认;管理员读取展示代理地址后可能再写回 | canonical 与 preview 分离,PUT 严格校验 |
app/admin/settings/page.tsx |
“恢复默认”重新选择旧域名 | 改为 /brand/... |
app/api/favicon/route.ts |
旧地址失败后用透明 1×1 冒充成功 | 改为真实本地 favicon |
components/Header.tsx、登录/注册/完善资料页 |
远端图坏时没有稳定回退,alt 被挤成竖排坏图文字 | 统一使用一次性回退组件 |
app/api/system-settings/upload-logo/route.ts |
只信浏览器 MIME、未限制大小、上传后未确认对象已持久化且元数据一致,权限口径也不一致 | 限制字节/像素/尺寸并完整解码、核对 MIME、带凭据 HEAD 核对大小和类型后再写库、统一系统管理员权限 |
app/api/restore-data/route.ts |
在线流程直接把旧桶 URL 写进新素材 | 永久停用在线写入入口 |
.kiro/steering/项目记忆库.md |
把已下线资源域名写成新素材正式入口,并用“老素材不改”覆盖了系统 Logo | 更新当前拓扑,明确 Logo 不属于老素材 |
.kiro/specs/production-system-analysis.md |
2025 年快照外观像当前正式说明 | 顶部标记为历史快照并指向当前文档 |
| 存储迁移文档 | 只验收素材,遗漏站点品牌资源和设置往返 | 补齐迁移门禁 |
| Git 历史中的旧生产报告 | 既会误导当前判断,也存在历史秘密风险 | 登记风险;当前不贸然改写远端历史 |
已采取的代码门禁
- 构建前执行
npm run storage:legacy-boundary。 - 旧域名只允许出现在明确列出的历史只读兼容代码、兼容性测试和不可修改的历史 migration 中。
- 在线历史恢复 API 永久停用;历史恢复改走显式来源、显式目标、默认 dry-run、先迁移对象再写库的离线流程。
- 品牌资源使用镜像内本地文件作为最终回退。
该门禁由两层组成:普通构建会扫描进入 Docker 构建上下文的源码、数据库 migration 和常见配置;正式增量发布会在工作站完整仓库上先扫描全部受支持源码/脚本,再允许联网构建。普通说明文档、未知扩展名及 Git 历史仍需独立审阅与秘密扫描,不能把规则匹配当成语义证明或历史清理工具。
合法的历史兼容范围
以下用途仍可识别旧域名,但不得生成新的旧域名数据:
- 媒体代理与 URL 改写
- 工作审核和考核调研中的历史素材展示
- 历史资源网络探测
- 旧对象迁移到当前桶的离线工具
- 对应的兼容性测试与不可变历史 migration
新增允许项必须在代码评审中说明用途并加入显式 allowlist,不能用“老素材兼容”作为默认值、上传目标、重置值或新写入的理由。
后续验收
- 当前行值和首页显示已验收;仍须执行受控 migration,并确认三个目标 migration 均有成功记录、Logo 两列默认值已改为
/brand/...,再逐行复核只存在本地或当前可信桶地址。 - Logo 配置 URL 404、超时或返回 HTML 时,所有入口仍显示可见本地品牌图。
- 管理员只改颜色/维护通知并保存,不得改变数据库中的 canonical Logo URL。
- 新建系统设置行和“恢复默认”均不得出现旧域名。
- 构建门禁能阻止旧域名重新进入非只读代码;唯一受控写引用只能是本 migration 的精确品牌数据清洗 SQL。
- 当前桶切换或存储迁移必须一并核对素材预览、品牌资源、favicon、设置 API、默认值和重置值。
- Logo 上传验收必须再从公网 URL 实际 GET,确认 HTTP 200、图片 Content-Type 和浏览器可解码;带凭据 HEAD 成功不能替代用户侧可见性验收。