# 许可证链路安全加固记录 更新时间:2026-06-02 ## 1. 本轮结论 本轮重点修复的是“本地数据库被直接写入后,伪造许可证变成专业版”的风险。 修复后,主应用不再只判断“许可证签名是否正确”,还会额外核对签名许可证里的绑定信息: - 项目编码必须匹配当前 `LICENSE_PROJECT_CODE` - 激活码必须匹配本地保存的 `licenseKey` - 实例 ID 必须匹配当前系统持久化的 `data/.instance-id` - 签名许可证必须包含 `fingerprintHash` 缺少这些绑定字段的旧许可证缓存会被视为无效,需要重新激活。 ## 2. 已完成的代码加固 - `license-server` 激活、免费注册、在线验证、心跳续租返回的许可证,都会把 `projectCode`、`licenseKey`、`instanceId`、`fingerprintHash`、`activationId` 写入 RSA 签名数据。 - 主应用保存许可证前,会校验签名数据里的项目、激活码和实例 ID 是否一致。 - 主应用每次从 `system_configs.key = license` 读取本地许可证时,也会重新做上述绑定校验。 - 旧的无绑定签名许可证、本地激活码不匹配、实例 ID 不匹配,都会被拒绝。 - `license-server/.dockerignore` 已补充 `.env*`、私钥和密钥目录排除,避免单独构建许可证服务镜像时把本地密钥带入镜像上下文。 ## 3. P1/P2 继续加固状态 本轮继续处理了渗透模拟里 P1/P2 中属于产品侧能落地的部分。NAS 管理员密码、Docker 管理权限和客户主机 root/admin 权限不作为应用代码可完全防守边界,但默认交付口子继续收紧: - 后台高风险维护接口继续要求最高权限账号、专业版系统设置能力,并增加人工确认短语: - `/api/admin/cleanup-database`:`清理全部素材` - `/api/admin/reset-material-rankings`:`重置排行榜数据` - `/api/admin/migrate-material-fields`:`执行字段迁移` - `upload-logo`、清库、重置排行榜、字段迁移都已纳入许可证覆盖测试,避免以后误删服务端许可证检查。 - `/api/restore-data` 继续保持默认关闭,并追加维护窗口保护:恢复令牌至少 32 字符、令牌固定时间比较、默认不信任可伪造的 `x-forwarded-for`、必须配置来源 IP 白名单、必须带 `x-restore-data-confirm: RESTORE_DATA`。 - `docker-compose.prod.yml` 中 MinIO 管理控制台默认只绑定 `127.0.0.1`,素材 API 端口仍按 `S3_PUBLIC_DOMAIN` 需要对用户浏览器开放。 - `home-filter-fields` 已经使用白名单字段和安全查询路径,并由测试确认不再调用 `$queryRawUnsafe`。 ## 4. 代码混淆和客户镜像边界 当前不是“完全没做混淆”。生产镜像构建会执行 `npm run build:prod`,在 Next.js 构建后对服务端 chunks 和许可证 API 产物做保守混淆;浏览器 source map 默认关闭。 本轮继续收紧了两个交付边界: - 运行镜像不再额外复制 `/app/lib` 源码目录,避免“构建产物已混淆,但客户镜像里又带一份未混淆源码”。 - 混淆脚本如果找不到目标文件,或任何目标文件混淆失败,会直接阻断生产出包。 注意:代码混淆只能提高逆向成本,不能当成许可证安全边界。真正的安全边界仍然是验证服务器签名、实例绑定、本地缓存校验、服务端权限和许可证检查。 ## 5. 仍然不能靠应用代码完全解决的边界 如果攻击者已经拥有客户 NAS 的管理员权限、Docker 管理权限或数据库管理权限,他就能修改容器、替换镜像、直接改数据库,应用自身无法完全防住这种本机最高权限攻击。 商业化防护的正确边界是: - 客户侧本地缓存只作为离线短期租约和防误改机制 - 许可证最终可信来源是验证服务器 - RSA 私钥只能存在于验证服务器运行环境 - 客户侧只保留 RSA 公钥和本地缓存加密密钥 - 本地缓存必须绑定项目、激活码和机器实例 - 付费模块核心能力继续依赖验证服务器签发的实例绑定清单和授权凭证 ## 6. 运维必须处理的事项 本轮代码已经阻断低成本本地伪造,但如果本地或历史环境里曾经暴露过真实密钥,仍必须轮换: 1. 重新生成许可证服务 RSA 密钥对。 2. 更新 `license-server` 运行环境里的 `RSA_PRIVATE_KEY`。 3. 更新主应用运行环境里的 `LICENSE_PUBLIC_KEY`。 4. 轮换 `LICENSE_REQUEST_SIGNING_SECRET`、`LOCAL_ENCRYPTION_KEY`、数据库密码、对象存储密钥、`NEXTAUTH_SECRET`。 5. 要求已激活环境重新激活,生成带绑定字段的新许可证缓存。 6. 客户 NAS 禁用弱密码 SSH,避免暴露公网 SSH,必要时改成密钥登录。 注意:不要把真实 `.env`、私钥、数据库连接串或服务器密码写入 Git、文档、工单或截图。 ## 7. 验证记录 本轮已验证: - `npm run type-check` - `npx tsc --noEmit`,在 `license-server/` 目录执行 - `npm test -- --runInBand lib/__tests__/license-service.test.ts app/api/license/activate/__tests__/route.test.ts app/api/license/activate-free/__tests__/route.test.ts` - `npm test -- --runInBand app/api/admin/__tests__/destructive-actions-superadmin.test.ts app/api/restore-data/__tests__/route.test.ts lib/__tests__/license-route-coverage.test.ts __tests__/production-build-hardening.test.ts __tests__/deployment-compose.test.ts` 验证重点: - 无绑定字段的旧许可证会被拒绝 - 绑定到其他实例的许可证会被拒绝 - 激活接口收到绑定信息不匹配的许可证时不会保存到本地 - 免费激活接口收到绑定信息不匹配的许可证时不会保存到本地 - 高风险后台接口必须保留确认短语 - 恢复接口默认不信任可伪造代理 IP 头,且必须带确认头 - 生产镜像不能额外复制未混淆源码目录