01 · 交付清点
收到的全部东西:两个压缩包
| w2r.site.zip | CMS 后台系统 | 241,815,795 字节 |
| demo.w2r.site.zip | 官网前台 | 101,044,255 字节 |
| 交接文档 | — | 无 |
| 部署说明 | — | 无 |
| 数据库导出 | — | 无 |
| 图片视频文件 | — | 无 |
成色:后端是新的,官网是旧的
能证明交付的不是最新版
对方自己的变更记录写到 6 月 21 日,里面明确还在改官网首页文案和标题。但交付的官网前端源码停在 6 月 12 日,官网后端更是停在 3 月 23 日。
更直接的两条:变更记录里写着有「首页正文接口」和「外部预览页」,在交付的代码里搜索结果都是 0 处。而后台那一侧的预览功能是完整实现的——说明后端做好了,前台承接页面没给。
03 · 安全问题
83 项安全相关问题,11 项属于最严重级别

下面每一条都标注了技术名称和大白话说明。这些在国家的等级保护检查里会被直接判不合格。
国家规定:在中国境内运营网站要按重要程度分等级、达到对应安全标准、请有资质机构测评、去公安局备案。没通过不能合法上线。本项目文档写的是「等保二级导向」——「导向」的意思是设计时参考了标准,但既没做过测评也没备案。
CSRF 防护被整段注释掉
已修复别人能冒充你在后台操作
详情
缺陷记录
CSRF 过滤器被注释掉,而 AuthFilter 保留 Session 兜底认证 → 全部写接口可被跨站伪造
影响
globals.before 里 'csrf'、'invalidchars' 全被注释(第 82-83 行),after 里 'secureheaders' 也注释(第 87 行)。若只用 Bearer token 风险有限,但 AuthFilter.php:31-35 与 ApiController.php:92 都保留了 session()->get('user_id') 兜底,Session Cookie 会随跨站请求自动带上(Cookie.php:90 samesite=Lax,POST 跨站虽被 Lax 拦一部分,但同站子域/表单场景仍可打)。加上 Cookie.php:57 secure=false、App.php:160 forceGlobalSecureRequests=false,明文 HTTP 下 Cookie 可被嗅探。建议要么彻底删掉 Session 兜底只认 JWT,要么开启 csrf。
位置
w2r.site/backend/app/Config/Filters.php:82
缺陷记录
交接件里的 .env 是 CI_ENVIRONMENT = development
影响
development 环境下 app/Config/Boot/development.php:14 打开 display_errors、:34 令 CI_DEBUG=true,任何未捕获异常都会渲染带完整堆栈与配置的调试页(含 Database 连接信息);Security.php:85 的 CSRF 失败重定向也随之关闭;Events.php:60 会额外注册 __hot-reload 路由。仓库里没有任何生产用 .env 或填好的模板(env 文件 69 行全是注释),接手方无从判断线上真实取值,误把这份 .env 带上生产即等于开着调试模式对外服务。
位置
backend/.env:5
登出不使 JWT 失效(会话撤销表只写不读)
已修复点了退出,账号还能再用 2 小时
详情
缺陷记录
JWT 会话撤销表只写不读:登出、并发登录踢人、禁用账号都不会让已签发 token 失效
影响
AuthFilter 只做 verify(签名+exp),从不查 user_sessions.revoked_at;UserSessionService 里根本没有 isRevoked() 方法。AuthController::logout 第 298 行调 revokeSession 写了 revoked_at,UserSessionService.php:76-82 在关闭并发登录时也批量 revoke,但两者对后续请求毫无作用——旧 token 在 exp 之前照常通行。同时 UserController::update/updateStatus/delete 从不调 revokeAllForUser,禁用或删除用户后其 token 仍可用。安全审计里「登出/踢人/禁用」三条控制全部失效。
位置
w2r.site/backend/app/Filters/AuthFilter.php:20
缺陷记录
MFA 校验与预览登录都没有次数限制,6 位 TOTP 与 6 位备用码可暴力破解
影响
mfaVerify 全程没有调用 LoginThrottleService,允许 ±1 个时间窗(AuthService.php:240)等于每 90 秒有 3 个有效码;备用码是 8 个 6 位纯数字(AuthService.php:107),空间与 TOTP 相同且校验成功即消耗。/api/auth/login 有图形验证码 + IP 锁定,但 PreviewController::access(第 167 行,Routes.php:40 公开路由)是另一个完整的用户名+口令校验入口,只有 IP 节流没有验证码,且 recordFailure 只在 code===3205 时调用(第 205-207 行),token 不存在/已撤销/已过期都不计次,可无限枚举预览 token。
位置
w2r.site/backend/app/Controllers/Api/AuthController.php:453
MFA 密钥与备用码仅 base64 编码
已修复手机验证码的钥匙,等于明文存在数据库
详情
缺陷记录
MFA 密钥与备用码仅 base64 编码,注释谎称「加密存储」
影响
第 110 行注释写「加密存储(Base64编码)」,实际 base64_encode 是编码不是加密。任何拿到 mfa_secrets 表读权限的人(DBA、备份文件、SQL 注入、误导出)都能一步还原 Base32 TOTP 种子并持续生成有效验证码,同时拿到全部 8 个明文备用码,MFA 这层防护等于零。修复需要引入 CI4 Encryption 服务并对存量行做一次性重加密迁移。
位置
w2r.site/backend/app/Services/AuthService.php:110-112(写入)、:217-221(读取)
缺陷记录
MFA 密钥经 URL 发送到第三方 api.qrserver.com
影响
把包含 TOTP 共享密钥的 otpauth:// URI urlencode 后拼进 https://api.qrserver.com/v1/create-qr-code/?data=... 并打印给运维点开。只要有人点这个链接,超管的 MFA 种子就以明文查询串形式出站到境外第三方,并留在对方访问日志里。ResetUserMfa.php:86 同一处问题。与 App.php:201 开启 CSP、ContentSecurityPolicy.php:139 connectSrc='self'「禁止外联」的安全基线自相矛盾。
位置
backend/app/Commands/SetupSuperAdminMfa.php:111
栏目管理六个接口零权限校验
需甲方定任何登录的人都能改网站导航
详情
缺陷记录
栏目管理全部写接口没有任何权限校验,任意登录角色可增删改栏目
影响
create(111)、update(164)、delete(237)、reorder(43)、index(31)、show(97) 六个方法体内没有一处 requirePermission/requireRoles,只有 rolePermissions(278) 和 updateRolePermissions(307) 校验了 rbac:manage。路由 Routes.php:65-70 只挂了 auth 过滤器。结果:audit_readonly 这种纯只读角色也能 DELETE /api/channels/:id 删掉栏目、能 PUT reorder 打乱整站导航。ChannelModel::hasContent 只挡有内容的栏目,空栏目直接删。
位置
w2r.site/backend/app/Controllers/Api/ChannelController.php:111
缺陷记录
ChannelController 栏目增删改查无权限校验,任意登录用户可改站点导航
影响
index/reorder/show/create/update/delete 六个方法零校验,只有 rolePermissions(:278)与 updateRolePermissions(:307)检查 rbac:manage。任意登录用户可以新建栏目、改栏目 slug、拖动整棵导航树(reorder 批量改 parent_id/sort_order),删除无内容栏目。栏目 slug 是前台 URL 的组成部分,改动直接影响线上站点结构与 SEO。
位置
backend/app/Controllers/Api/ChannelController.php:31,43,97,111,164,237
内容管理五个写接口零权限校验
已修复任何登录的人都能建、改、删内容
详情
缺陷记录
ContentController 五个写接口完全没有权限码校验,任意已登录用户可创建/改/删内容
影响
路由只挂 auth 过滤器(Routes.php:75-81),控制器内没有任何 requirePermission/requireRoles。只要登录并通过 MFA,任何角色(含只读的 audit_readonly)都能调 POST /api/content 建内容、调 PUT/DELETE 改删自己创建的内容(:493 与 :719 只比对 created_by 或 sys_admin)。RBAC 里 content:create / content:edit / content:delete 三个权限码在整个内容主流程上形同虚设,等保口径下属于越权。
位置
backend/app/Controllers/Api/ContentController.php:40,130,304,474,705
缺陷记录
前台正文 HTML 只在浏览器端用自研消毒函数过滤,iframe srcdoc 等可穿透
影响
NewsArticleContent.vue:32 用 v-html 渲染 bodyHtmlSafe,消毒全靠这个 27 行的自研函数:只删 script/style/link[rel=stylesheet]、只去 on* 属性、只查 href 的 javascript:。未处理 <iframe srcdoc="...">、<object>、<embed>、src="javascript:"、formaction、SVG 的 xlink:href。而 CMS 写入侧(ContentController.php:379-382 / 544-546)对 content_json 不做任何过滤,只是 json_encode。任何 editor 角色可在正文注入持久化 XSS,打到官网所有访客。DOMParser 不可用时(SSR/老浏览器)第 11-12 行只做一个正则删 script,形同虚设。应改用 DOMPurify 并在服务端入库时也过滤一次。
位置
demo.w2r.site/frontend/src/lib/news/sanitizeArticleHtml.js:14
DAM 上传的 upload_id 未做格式校验
已修复上传功能可以往服务器任意位置写文件
详情
缺陷记录
审计验收命令用 JwtService 凭空伪造 sys_admin token,绕过密码与 MFA
影响
$jwt->generate(1, 'sys_admin', true) 直接签发 user_id=1、role=sys_admin、mfa_verified=true 的合法 JWT(JwtService.php:48 签名为 generate(int $userId, string $role, bool $mfaVerified))。AuditAcceptanceExtended.php:48-49 同样。等于在代码库里留了一条「无需口令即可拿到最高权限令牌」的路径;只要能跑 spark 或能复制这三行逻辑,整套 RBAC/MFA 形同虚设。
位置
backend/app/Commands/AuditAcceptance.php:50
缺陷记录
DAM 分片上传的 upload_id 未做任何格式校验,直接当目录名与文件名拼接 → 任意路径写文件(可 getshell)
影响
DamController::chunk 第 50 行把请求里的 upload_id 原样传下来,saveChunk 第 106 行 $chunkDir = chunksDir . '/' . $uploadId 并用 mkdir(recursive) 创建,第 111 行写 {$chunkIndex}.part;merge 第 202 行又拼 $targetDir . '/' . $uploadId . '_' . $safeName。upload_id 传 ../../../public/x 即可把内容写到 web 根下。配合下条 resolveAssetType 的 OR 判定(mime 传 image/png 就放行 .php 扩展名),任何拿到 dam:upload 权限的账号可写入并执行 PHP,等于远程代码执行。
位置
w2r.site/backend/app/Services/DamUploadService.php:106
资产类型判定用 OR 而非 AND
已修复伪装成图片的文件能通过上传检查
MFA 验证接口无次数限制
已修复6 位动态码可以无限次猜
详情
缺陷记录
MFA 校验与预览登录都没有次数限制,6 位 TOTP 与 6 位备用码可暴力破解
影响
mfaVerify 全程没有调用 LoginThrottleService,允许 ±1 个时间窗(AuthService.php:240)等于每 90 秒有 3 个有效码;备用码是 8 个 6 位纯数字(AuthService.php:107),空间与 TOTP 相同且校验成功即消耗。/api/auth/login 有图形验证码 + IP 锁定,但 PreviewController::access(第 167 行,Routes.php:40 公开路由)是另一个完整的用户名+口令校验入口,只有 IP 节流没有验证码,且 recordFailure 只在 code===3205 时调用(第 205-207 行),token 不存在/已撤销/已过期都不计次,可无限枚举预览 token。
位置
w2r.site/backend/app/Controllers/Api/AuthController.php:453
缺陷记录
MFA 重置后再次验证会因 in_array(null) 抛 TypeError,接口 500
影响
resetMfa(第 304-308 行)把 backup_codes 置成空字符串 ''。之后 verifyMfaCode 第 221 行 json_decode(base64_decode(''), true) 得到 null,第 222 行 in_array($code, $backupCodes, true) 在 PHP 8 下抛 TypeError(haystack 必须是 array)→ /api/auth/mfa/verify 返回 500 而不是「验证码错误」。用户自助重置 MFA 后走注册流程会撞上。同一处第 217 行注释写「解密密钥」但实际只是 base64_encode(generateMfaSecret 第 111 行),MFA 种子在库里等同明文。
位置
w2r.site/backend/app/Services/AuthService.php:221
JWT 可从网址参数传入
已修复登录凭证会被写进服务器日志
详情
缺陷记录
JWT 接受从 URL 查询参数传入,token 会落进访问日志、Referer 和浏览器历史
影响
getTokenFromRequest 第 196-199 行在 Authorization 头和 Cookie 之后还支持 ?token=。任何带 token 的链接被分享、被 nginx access_log 记录、或页面里有外链导致 Referer 外泄,都会泄露一个完整有效的管理会话;配合上面「撤销不生效」,泄露后无法回收。同时 verify()(第 86-89 行) 只验签名和 exp,没有校验 iss / aud(config 里定义了但没用上),同密钥签发的其它用途 token 会被接受。
位置
w2r.site/backend/app/Services/JwtService.php:196
缺陷记录
JWT 可从 ?token= 查询串与 jwt_token Cookie 取值,且 CSRF 全局关闭
影响
getTokenFromRequest 依次尝试 Authorization 头、jwt_token Cookie、?token= 查询参数。查询串携带令牌会进 Nginx/Apache access log 与 Referer;Cookie 路径配合 Config/Filters.php:80-84 里被注释掉的 csrf/invalidchars 全局过滤器,构成可被 CSRF 利用的写接口面(当前前端用 localStorage + Bearer 头,属于「暂时没踩到」而非「不存在」)。
位置
backend/app/Services/JwtService.php:196-199
账号禁用与角色降级在有效期内不生效
已修复停用了账号,他还能用 2 小时
详情
缺陷记录
ContentController 五个写接口完全没有权限码校验,任意已登录用户可创建/改/删内容
影响
路由只挂 auth 过滤器(Routes.php:75-81),控制器内没有任何 requirePermission/requireRoles。只要登录并通过 MFA,任何角色(含只读的 audit_readonly)都能调 POST /api/content 建内容、调 PUT/DELETE 改删自己创建的内容(:493 与 :719 只比对 created_by 或 sys_admin)。RBAC 里 content:create / content:edit / content:delete 三个权限码在整个内容主流程上形同虚设,等保口径下属于越权。
位置
backend/app/Controllers/Api/ContentController.php:40,130,304,474,705
缺陷记录
栏目管理全部写接口没有任何权限校验,任意登录角色可增删改栏目
影响
create(111)、update(164)、delete(237)、reorder(43)、index(31)、show(97) 六个方法体内没有一处 requirePermission/requireRoles,只有 rolePermissions(278) 和 updateRolePermissions(307) 校验了 rbac:manage。路由 Routes.php:65-70 只挂了 auth 过滤器。结果:audit_readonly 这种纯只读角色也能 DELETE /api/channels/:id 删掉栏目、能 PUT reorder 打乱整站导航。ChannelModel::hasContent 只挡有内容的栏目,空栏目直接删。
位置
w2r.site/backend/app/Controllers/Api/ChannelController.php:111
VW 的自动安全扫描配置写坏了
能修代码从没被安全扫描检查过
详情
缺陷记录
核心功能代码从未提交,GitLab 远端不存在这批实现
影响
admin-frontend 的 git HEAD 是 2026-05-25 的 b033e30,工作区有 8 个文件被修改(channel.js、content.js、figma.js、RichTextEditor.vue、Channel.vue、Content.vue、Media.vue、News.vue,共 675 行新增),另有 6 个一级功能文件完全未入库:api/preview.js、api/publish.js、composables/useContentPublish.js、composables/useContentEditDraft.js、composables/useContentDeleteDraft.js、utils/contentPreview.js(合计 231 行)。发布、预览 token、修订稿三套核心流程全在这批未提交的代码里。远端 gitlab.onedevops.vw.com.cn/c-sdxx/corporate-website/frontend 上没有它们,交接的这份工作区是唯一副本,任何误删都不可恢复。接手第一件事应是提交入库。
位置
js/composables/useContentPublish.js:1
认证/权限/工作流/发布/DAM 零测试覆盖
能修改任何东西都没法确认有没有改坏
详情
缺陷记录
测试只覆盖 5 个 Figma 服务类,认证/RBAC/工作流/发布/DAM 零覆盖
影响
tests/ 下 8 个测试文件共 33 个测试方法、79 条真实断言,其中 6 个文件全部属于 Figma 导入链路,另两个(ExampleDatabaseTest、ExampleSessionTest)是 CI4 骨架自带示例,HealthTest 只校验 APPPATH 常量与 baseURL 是合法 URL。对应 app/ 下 24 个 API 控制器、32 个 Service、25 个 Model、3 个 Filter、51 个迁移无任何测试。已知的 EntryModel ?datetime cast 崩溃、创建内容非事务性这两个缺陷,正是因为没有工作流/发布冒烟测试才漏到线上。
位置
backend/phpunit.xml.dist:26
审计验收命令会伪造管理员身份
能修有个命令能凭空造出超级管理员权限
详情
缺陷记录
CSRF 过滤器被注释掉,而 AuthFilter 保留 Session 兜底认证 → 全部写接口可被跨站伪造
影响
globals.before 里 'csrf'、'invalidchars' 全被注释(第 82-83 行),after 里 'secureheaders' 也注释(第 87 行)。若只用 Bearer token 风险有限,但 AuthFilter.php:31-35 与 ApiController.php:92 都保留了 session()->get('user_id') 兜底,Session Cookie 会随跨站请求自动带上(Cookie.php:90 samesite=Lax,POST 跨站虽被 Lax 拦一部分,但同站子域/表单场景仍可打)。加上 Cookie.php:57 secure=false、App.php:160 forceGlobalSecureRequests=false,明文 HTTP 下 Cookie 可被嗅探。建议要么彻底删掉 Session 兜底只认 JWT,要么开启 csrf。
位置
w2r.site/backend/app/Config/Filters.php:82
缺陷记录
审计验收命令用 JwtService 凭空伪造 sys_admin token,绕过密码与 MFA
影响
$jwt->generate(1, 'sys_admin', true) 直接签发 user_id=1、role=sys_admin、mfa_verified=true 的合法 JWT(JwtService.php:48 签名为 generate(int $userId, string $role, bool $mfaVerified))。AuditAcceptanceExtended.php:48-49 同样。等于在代码库里留了一条「无需口令即可拿到最高权限令牌」的路径;只要能跑 spark 或能复制这三行逻辑,整套 RBAC/MFA 形同虚设。
位置
backend/app/Commands/AuditAcceptance.php:50
调试脚本残留在公网可访问位置
能修有个网页会泄露服务器配置信息
详情
缺陷记录
public/test-putenv.php 调试脚本残留在 web 根目录,未鉴权
影响
任何人访问 https://<域名>/test-putenv.php 即可拿到 PHP 版本、SAPI、完整 disable_functions 列表,以及 system/Config/DotEnv.php 第 98 行的源码内容。属于典型的信息泄露与攻击面探测入口,上线前应删除。
位置
backend/public/test-putenv.php:1
全站未强制 HTTPS,Cookie 未加安全标记
能修登录信息可能以明文在网络上传输
详情
缺陷记录
CSRF 过滤器被注释掉,而 AuthFilter 保留 Session 兜底认证 → 全部写接口可被跨站伪造
影响
globals.before 里 'csrf'、'invalidchars' 全被注释(第 82-83 行),after 里 'secureheaders' 也注释(第 87 行)。若只用 Bearer token 风险有限,但 AuthFilter.php:31-35 与 ApiController.php:92 都保留了 session()->get('user_id') 兜底,Session Cookie 会随跨站请求自动带上(Cookie.php:90 samesite=Lax,POST 跨站虽被 Lax 拦一部分,但同站子域/表单场景仍可打)。加上 Cookie.php:57 secure=false、App.php:160 forceGlobalSecureRequests=false,明文 HTTP 下 Cookie 可被嗅探。建议要么彻底删掉 Session 兜底只认 JWT,要么开启 csrf。
位置
w2r.site/backend/app/Config/Filters.php:82
缺陷记录
MFA 密钥经 URL 发送到第三方 api.qrserver.com
影响
把包含 TOTP 共享密钥的 otpauth:// URI urlencode 后拼进 https://api.qrserver.com/v1/create-qr-code/?data=... 并打印给运维点开。只要有人点这个链接,超管的 MFA 种子就以明文查询串形式出站到境外第三方,并留在对方访问日志里。ResetUserMfa.php:86 同一处问题。与 App.php:201 开启 CSP、ContentSecurityPolicy.php:139 connectSrc='self'「禁止外联」的安全基线自相矛盾。
位置
backend/app/Commands/SetupSuperAdminMfa.php:111
审计日志可被应用账号修改删除
能修出了事,记录可能已经被改过
详情
缺陷记录
JWT 会话撤销表只写不读:登出、并发登录踢人、禁用账号都不会让已签发 token 失效
影响
AuthFilter 只做 verify(签名+exp),从不查 user_sessions.revoked_at;UserSessionService 里根本没有 isRevoked() 方法。AuthController::logout 第 298 行调 revokeSession 写了 revoked_at,UserSessionService.php:76-82 在关闭并发登录时也批量 revoke,但两者对后续请求毫无作用——旧 token 在 exp 之前照常通行。同时 UserController::update/updateStatus/delete 从不调 revokeAllForUser,禁用或删除用户后其 token 仍可用。安全审计里「登出/踢人/禁用」三条控制全部失效。
位置
w2r.site/backend/app/Filters/AuthFilter.php:20
缺陷记录
审计验收命令用 JwtService 凭空伪造 sys_admin token,绕过密码与 MFA
影响
$jwt->generate(1, 'sys_admin', true) 直接签发 user_id=1、role=sys_admin、mfa_verified=true 的合法 JWT(JwtService.php:48 签名为 generate(int $userId, string $role, bool $mfaVerified))。AuditAcceptanceExtended.php:48-49 同样。等于在代码库里留了一条「无需口令即可拿到最高权限令牌」的路径;只要能跑 spark 或能复制这三行逻辑,整套 RBAC/MFA 形同虚设。
位置
backend/app/Commands/AuditAcceptance.php:50
04 · 已完成
系统已跑通,14 项问题已修复

系统已在本机完整启动,登录、双重验证、权限控制、审计记录全链路验证通过。每一处修复都有修改前后的实测输出,不是方案建议。
修复速度是实测的
14 个问题做了完整的「先复现、再修改、再回归」流程并计时:中位 7 分钟,最慢 11.9 分钟,没有一个超过 12 分钟。每项至少 5 组回归测试。
05 · 还差什么
分成三类,性质完全不同

一、只在对方服务器上的(交东西,不是开发)
这几样是把已有的文件交出来,不涉及任何新的开发工作。
1网站上所有的图片和视频
对方服务器上有一个叫 writable 的文件夹,网站用过的每一张图、每一个视频、每一份 PDF 都存在那里。这个文件夹整个没打包给我们。
拿不到会怎样:日志显示光视频就有 197 个,是花了 11 个小时从老网站搬过来的。拿不到就得全部重新搬一遍,而老网站随时可能下线。
详情
行动项
打包并交付 /www/wwwroot/w2r.site/backend/writable/ 整个目录,重点是 uploads/dam/ 与 uploads/page-css/;交付前先统计体积与文件数,与 assets 表记录数对账
原因
unzip -l 显示该路径在压缩包里有 0 个条目。DAM 物理文件与 Figma 页面 CSS 全部缺失,且 CI4 缺 writable/cache|logs|session 连启动都会失败;demo 的 shared/dam-writable 已是断链符号链接
责任方
交付方 · 上线前必须
行动项
把 CI_ENVIRONMENT 改为 production,并逐项核对 §7 差异表(display_errors、CI_DEBUG、Debug Toolbar、__hot-reload、Cookie secure、forceGlobalSecureRequests、opcache、CORS、robots.txt)
原因
两站 .env:5 都是 development;demo 的 writable/debugbar/ 里有 425 个文件,证明这台对外机器确实以 debug 模式接过真实请求,异常页会回显绝对路径与内部栈。
责任方
接手团队 · 上线前必须
2网站的全部内容数据
数据库导出文件。我们把两个压缩包翻遍了,一个都没有。
拿不到会怎样:350–400 篇中英文新闻、首页文案、栏目结构、所有账号、审计记录,全部为零。表格结构能重建,里面的内容不能。
详情
行动项
从 47.242.46.253 导出完整数据库(mysqldump 全库,含结构与数据),并单独导出 config、channels、entries、entry_i18n、assets、asset_entry_i18n、users、mfa_secrets、roles/permissions 八张表作为可校验子集
原因
全交付树零 .sql 文件。没有数据库,CMS 只是一具空壳:栏目树、内容、用户与 MFA 绑定、DAM 元数据、workflow 与 SEO 配置全部不存在
责任方
交付方(需甲方 IT 提供服务器访问) · 上线前必须
行动项
落实数据库与 DAM 媒体的备份方案(mysqldump 定时任务或迁 RDS + 卷快照),并验证一次恢复。
原因
docs/cms/log-inventory-excel-en.tsv:16 明写备份 out of app scope、由 operations 执行;但全仓无 .sql、无 mysqldump 脚本、无快照策略,backend/writable/ 整个目录也不在交付包里。当前状态是没有任何人能证明备份存在。
责任方
甲方 IT · 上线前必须
3官网前台现在真正在用的代码
我们拿到的这份,最后改动停在 3 月 23 日。但对方的变更记录里,6 月还在改首页。
拿不到会怎样:对比下来至少缺两块功能:首页正文的读取接口、外部预览页面。也就是说照现在这份代码,还原不出线上首页的样子。
详情
行动项
从服务器取回 demo.w2r.site 的当前生产代码(backend/public/api.php、frontend/src 全量、两份 .htaccess),与本次交付的 2026-03-23 快照做 diff,并把差异部分补齐交付
原因
交付的 demo 只有 5 个 API 端点和 2 条前端路由,而 CMS changelog 记录 4–6 月已上线 /api/home、/api/page、/api/preview、/api/page-css、PreviewPage.vue、Chronology 重排、媒体中心。三个月增量代码整体缺失,且 demo 全站无版本控制、无回滚点
责任方
交付方 · 上线前必须
4正式服务器的网址跳转配置
决定「用户访问某个网址时,交给哪段程序处理」的那份配置文件。
拿不到会怎样:代码里只有一种服务器格式的规则,而正式环境用的是另一种,两者不通用。没有这份配置,就不知道线上到底是怎么跑的。
详情
行动项
向交付方索取 47.242.46.253 上两个站点的 nginx 站点配置、php-fpm pool 配置、PHP 版本与 disable_functions 实际值,以及 crontab -l 与宝塔计划任务列表
原因
仓库中无任何站点配置(find 全树只在接手方自建的 localdev/ 下有 .conf);交付的改写规则全是 Apache 语法的 .htaccess,而 404 页与 frontend/README.md:24 都指向 nginx,nginx 不读 .htaccess——也就是说线上真正生效的路由规则、/api 转发、SPA fallback 目前一条都看不到。定时发布是核心功能,cron 是否配置过同样无从判断。
责任方
甲方 IT · 上线前必须
行动项
交付真实的生产 nginx 站点配置、防火墙策略与服务器 NTP 时钟同步配置
原因
生产疑似 nginx(404.html 是 nginx 默认页),但改写规则写在 Apache 语法的 .htaccess 里,nginx 不读。测评第一天就会索要这些配置;时钟同步是审计追溯的前提,必查
责任方
甲方 IT · 接手后 30 天内
5SSL 证书信息
网站地址栏那把小锁。仓库里没有任何证书文件,只有代码注释里提到过。
拿不到会怎样:需要知道是谁签发的、什么时候到期、谁负责续期。忘了续期网站会直接打不开。
详情
行动项
采购并上线安全设备:二级需防火墙、IPS/IDS、主机防病毒、日志审计(≥6 个月)、漏扫、SSL 证书;若定三级还需 WAF、堡垒机、数据库审计、准入控制、上网行为管理、集中安全管理/态势感知、核心设备冗余、异地备份
原因
当前是宝塔面板 + 单机部署形态(.user.ini 显示 open_basedir=/www/wwwroot/w2r.site/),离等保要求的架构差距很大。云上部署可用云厂商打包产品降低成本
责任方
甲方 IT · 可排期
行动项
向交付方索要生产 SSL 证书的签发主体、有效期、续期方式与续期责任人。仓库内无任何 .pem / .crt / .key / .pfx 文件,唯一痕迹是 demo.w2r.site/.well-known/acme-challenge/ 下一个 76 字节的验证文件(mtime 2026-02-28 23:44)。
原因
推断生产用的是宝塔面板一键签发的 90 天 ACME 证书。90 天证书的自动续期链条一旦断在乙方账号里,到期当天全站 HTTPS 报错,且接手方无从修复。
责任方
交付方 · 上线前必须
625 份工程规范文件
对方用 AI 工具开发时写的规则和技能文件,规格书第四章列了完整清单(15 条规则 + 10 个技能),实际只给了 2 条。
拿不到会怎样:这些是「这个项目该怎么写代码」的唯一权威说明。好消息是内容大部分能从已有的 26 份设计文档里还原出来。
详情
行动项
取回 /www/wwwroot/w2r.site/.cursor/(15 条 rules + 10 个 skills)与 /www/wwwroot/.cursor/rules/(workspace 级,含 005-demo-media-permissions-mfa.mdc 与 URL 语言约定)
原因
docs/project-introduction.md:266-299 有完整清单,压缩包里 .cursor 条目数为 0。分层约定、错误码分段、审计规范、媒体权限完整规则全部只剩标题
责任方
交付方 · 接手后 30 天内
行动项
索要交付物欠账中的 .cursor/ 全部 15 条 rules 与 10 个 skills 原文。当前只交付了 2 条 rules,且 media-permissions-mfa.mdc:13 自己指向一个缺失文件 005-demo-media-permissions-mfa.mdc。
原因
这些 rules 是代码库的隐性规范载体,包含像「改了 frontend 必须真的执行 npm run build,否则修改不生效」这类会直接导致线上不生效的约束。拿到全文之后才能判断要不要买 Cursor Teams 席位(32 美元/席/月年付)。
责任方
交付方 · 上线前必须
二、技术工作(我们自己做)
最大的一块:36 条网址跳转规则要重写
这套代码的网址跳转规则是按 Apache 格式写的,而正式环境用的是 nginx——两者配置写法完全不同,Apache 的规则文件 nginx 根本不读。这 36 条要逐条翻译。判断依据:代码里的错误提示页是 nginx 的默认样式。
我们自己能做的 19 件事
重新跑一次全量安全扫描(w2r.site/security-scan.sh + composer aud…
详情
完整描述
重新跑一次全量安全扫描(w2r.site/security-scan.sh + composer audit + npm audit),并把结果与 2026-01-25 那次对比
为什么要做
security-scan-results/ 全部 7 个文件时间戳都是 20260125_011114,而 Figma 导入、修订稿、预览改造等大量代码在 3–7 月才写,半年多新代码从未扫过
补做隐私政策、法律声明、Sitemap、FAQ、Help 五个页面,替换 SiteFooter.vue:…
详情
完整描述
补做隐私政策、法律声明、Sitemap、FAQ、Help 五个页面,替换 SiteFooter.vue:44-48 的 href='#' 占位
为什么要做
页脚已硬编码备案号(SiteFooter.vue:51-52:京ICP备08103718-21、京公网安备 11010502040869号),却指向空链接,是可被直接指出的合规缺口
把 CI_ENVIRONMENT 改为 production,并逐项核对 §7 差异表(display_…
详情
完整描述
把 CI_ENVIRONMENT 改为 production,并逐项核对 §7 差异表(display_errors、CI_DEBUG、Debug Toolbar、__hot-reload、Cookie secure、forceGlobalSecureRequests、opcache、CORS、robots.txt)
为什么要做
两站 .env:5 都是 development;demo 的 writable/debugbar/ 里有 425 个文件,证明这台对外机器确实以 debug 模式接过真实请求,异常页会回显绝对路径与内部栈。
删除 w2r.site/backend/public/test-putenv.php 与 groupui…
详情
完整描述
删除 w2r.site/backend/public/test-putenv.php 与 groupui-showcase.html
为什么要做
两个调试/演示文件留在 web 根目录,test-putenv.php 会对外回显 disable_functions 配置与 CI4 DotEnv 源码行。
把 app/Config/App.php:19 的 baseURL、admin-frontend/js/…
详情
完整描述
把 app/Config/App.php:19 的 baseURL、admin-frontend/js/utils/contentPreview.js:2 的 PUBLIC_SITE_ORIGIN、SeoSchemaService.php:20 与 AuditAcceptance.php:26 里硬编码的域名,改造成从 .env 或构建期变量读取
为什么要做
生产域名是 vgc.digirepub.com(docs/project-introduction.md:44),而代码里写死的是 w2r.site 与 demo.w2r.site。当前换域名等于改源码再重新构建,无法通过配置完成环境切换。
重新构建两个前端并核对产物:admin-frontend → backend/public/AdM(vi…
详情
完整描述
重新构建两个前端并核对产物:admin-frontend → backend/public/AdM(vite.config.js:11),demo frontend → dist
为什么要做
交付的 AdM 产物 mtime 是 2026-06-21 22:14,而 admin-frontend 源码最新到 2026-07-22 09:30,产物落后约一个月;demo dist 也落后 src 五分钟。直接部署交付树会让线上跑的后台与交付的源码对不上。
手工创建 w2r.site/backend/writable/ 全套子目录(logs、cache、ses…
详情
完整描述
手工创建 w2r.site/backend/writable/ 全套子目录(logs、cache、session、uploads/dam、uploads/dam_chunks、uploads/page-css、fonts)并设 0755、属主为 php-fpm 用户;同时把 demo 现有 0777 的 writable 收紧
为什么要做
整个 writable/ 未交付,缺任何一个子目录都会导致对应功能(日志、缓存、会话、DAM 分片、页面 CSS、验证码字体)在运行时报错;demo.w2r.site/backend/writable 及其全部子目录现为 drwxrwxrwx。
配置三条 crontab:publish:run-schedule 每分钟、audit:cleanup …
详情
完整描述
配置三条 crontab:publish:run-schedule 每分钟、audit:cleanup 与 logs:cleanup 每月 1 日 0 点;PHP 二进制写全路径,命令前先 cd 到 backend/
为什么要做
RunPublishSchedule.php:14、CleanAuditLogs.php:14、CleanApplicationLogs.php:15 都注明需要 cron;state-machine.md:292/:298 要求定时发布每分钟检查。仓库中无 crontab 交付,若线上从未配置,则定时发布/下线自始失效、日志永不清理。
为生产与测试拆分独立数据库与独立 DAM 目录,禁止再用跨站点符号链接共用媒体
详情
完整描述
为生产与测试拆分独立数据库与独立 DAM 目录,禁止再用跨站点符号链接共用媒体
为什么要做
两站 .env 指向同一 MySQL 的同一个库 vgc(w2r .env:26 / demo .env:12),DAM 靠 shared/dam-writable 直连同一份文件。docs/cms/requirements.md:87-89 明确要求数据库、上传目录、缓存按环境隔离,现状三项全部违反。
更换 backend/.env:37 的 JWT_SECRET 为随机高强度密钥(openssl ran…
详情
完整描述
更换 backend/.env:37 的 JWT_SECRET 为随机高强度密钥(openssl rand -base64 32),并把 backend/.env:5 的 CI_ENVIRONMENT 由 development 改为 production
为什么要做
当前签名密钥是人类可读的开发占位串,HS256 可离线爆破后伪造任意 sys_admin token;development 模式还会输出完整异常堆栈,且使 JwtService.php:30-36 的生产密钥护栏失效。这是全部已知问题里最严重的一条
为 ContentController(852 行)与 ChannelController(393 行)…
详情
完整描述
为 ContentController(852 行)与 ChannelController(393 行)的全部写接口补 requirePermission 调用,照 security-model.md:44-67 的权限矩阵,参照 DamController 的既有写法
为什么要做
两个控制器全文检索 hasPermission/requirePermission 零命中,路由 Routes.php:65-83 只挂 auth 过滤器。任何通过 MFA 的账号(含只读审计角色)都能改删内容与栏目,是典型越权,测评必判高风险项
在 AuthFilter.php 增加 user_sessions.revoked_at 校验,让登出、…
详情
完整描述
在 AuthFilter.php 增加 user_sessions.revoked_at 校验,让登出、并发登录控制与超时退出真正在服务端生效
为什么要做
UserSessionService.php:100-117 只写不读,AuthFilter.php:12-35 从不查。登出后 token 仍有效最长 2 小时;「禁止并发登录」开关形同虚设
MFA 种子改用 CI4 Encryption 真加密存储,备用码改加盐哈希 + 一次性 + 计入节流;…
详情
完整描述
MFA 种子改用 CI4 Encryption 真加密存储,备用码改加盐哈希 + 一次性 + 计入节流;删除 AuthService.php:110 那句「加密存储(Base64编码)」的假注释
为什么要做
当前仅 base64 编码,等同明文。备用码是 8 个静态 6 位数字、无时间窗、无节流,是 MFA 爆破的真实入口。假注释本身会让测评员质疑全套设计文档的可信度
给 /api/auth/mfa/verify 接入节流,并把 LoginThrottleService …
详情
完整描述
给 /api/auth/mfa/verify 接入节流,并把 LoginThrottleService 由纯 IP 维度改为「账户 + IP」双维度;同时增加账户级锁定字段与解锁流程
为什么要做
AuthController.php:453-495 全程无节流;LoginThrottleService.php 只按 IP 锁,换 IP 即绕过,共用出口 IP 又会互锁。等保身份鉴别「限制非法登录次数」判不符合
删除 backend/public/test-putenv.php 与 groupui-showcase…
详情
完整描述
删除 backend/public/test-putenv.php 与 groupui-showcase.html
为什么要做
test-putenv.php 公网可达并输出 PHP 环境与 disable_functions 清单,违反「最小安装原则」,任何漏扫工具都会命中
关闭 JwtService.php:189-199 的 URL query 与 Cookie 兜底 to…
详情
完整描述
关闭 JwtService.php:189-199 的 URL query 与 Cookie 兜底 token 传递,只保留 Authorization 头,并同步修改 security-model.md:358-363
为什么要做
token 进 URL 会落入 nginx access_log、Referer、浏览器历史;Cookie 兜底又让被注释掉的 CSRF 过滤器(Filters.php:82)成为真实缺口
修正传输与会话配置:App.php:160 forceGlobalSecureRequests 改 tr…
详情
完整描述
修正传输与会话配置:App.php:160 forceGlobalSecureRequests 改 true、Cookie.php:57 secure 改 true、Cookie.php:90 samesite 改 Strict、.env:49 regenerateDestroy 改 true、Session.php:47 expiration 与文档对齐;恢复 Filters.php 的 secureheaders
为什么要做
多处代码与 security-model.md:454-458、445 的描述不一致。文档写了没做,比没写更容易在测评的文档核对环节扣分
修正 demo.w2r.site/frontend/src/components/home/SiteFo…
详情
完整描述
修正 demo.w2r.site/frontend/src/components/home/SiteFooter.vue:51-52 的页脚:改成法定写法「京ICP备08103718号-XX」,加 beian.miit.gov.cn 链接,公安备案号加警徽图标与平台链接;并把它从硬编码常量改为 CMS 可配置项。
为什么要做
当前写法为 BJ ICP 08103718 -21,既不规范也没有链接,且这个号绑的是 volkswagengroupchina.com.cn 而非本项目任何域名。经 WebFetch 核实现网页脚为「京ICP备08103718号-21」「京公网安备 11010502040869号」,属同一主体但不同网站。
给 demo.w2r.site/frontend/src/styles/tokens.scss:19-2…
详情
完整描述
给 demo.w2r.site/frontend/src/styles/tokens.scss:19-21 的三条字体栈补上 CJK 字体。若拿不到商用中文字体授权,直接用思源黑体 / Noto Sans CJK SC(SIL OFL 1.1,明确允许 webfont 商用,0 元)。
为什么要做
The Group 只做了 Latin / Greek / Cyrillic 三套文字,没有汉字(renebieder.com 页面原文)。当前三条字体栈的兜底只有 sans-serif,中文完全由访客系统默认字体渲染,Mac / Windows / Android 三端呈现不一致。
把 demo.w2r.site/frontend/src/assets/figma/mcp-assets…
详情
完整描述
把 demo.w2r.site/frontend/src/assets/figma/mcp-assets.js:41-62 的 11 条 figma.com/api/mcp/asset/ 外链全部导出落盘,改为本地 import。
为什么要做
这些是新闻详情页的图片源(被 NewsArticleContent.vue 与 mockNewsArticle.js 引用),文件自己在 :1-3 就写明「过期前需替换或同步导出」。它同时违反 docs/cms/requirements.md:39 与 docs/cms/security-model.md:404 的禁止外联规定,且让 Figma 账号成为线上可用性的单点。
三、甲方内部流程(跟开发无关)
境内提供网站服务必须先备案:ICP 备案(工信部)和公安联网备案(属地公安),备案号要显示在网站底部。
境外服务器不需要也不能用境内备案号。现在代码把境内备案号写死在页面里,而页面由香港服务器提供——属于使用不当。
这几项需要甲方启动,不启动代码修得再好也上不了线
等保定级备案与测评 · ICP 与公安备案主体确认 · 品牌字体的网页使用授权 · 安全设备采购 · 数据库与媒体的备份方案
06 · 详细资料
给技术同事看的
四项安全修复的完整技术记录(含修复前后实测输出)
CSRF 防护全局启用(含 secureheaders / invalidcha
方案
【结论】三个全局过滤器已启用并跑通:跨站伪造请求由 200 变 403,后台登录+MFA、二次确认、内容写、受限资产交付、官网前台 17 项回归全过。
【方案】按「凭证类型」做非对称防护,而不是给所有 API 一刀切上 token。判定链(Config\Filters.php 末尾的 CsrfGuard):
- GET/HEAD/OPTIONS/TRACE 直接放行(与框架 Security::verify 口径一致)。
- 请求不带「浏览器自动附带的凭证」(w2r_cms_session、jwt_token cookie)→ 放行。纯 Bearer 客户端、服务端集成完全不受影响:浏览器不会自动加 Authorization 头,构不成 CSRF。
- 带 cookie 的写请求先过 Origin/Referer 同源校验(对比请求自身 Host + App::$baseURL 主机 + Security::$trustedOrigins)。Origin: null(沙箱 iframe)判为不可信。两个头都没有 → 判为非浏览器客户端,交给第 4 层。
- 再判是否要 token:非严格模式下,Authorization 头里有验签通过的 Bearer → 免(持有型凭证,跨站伪造不出来);api/auth/login、api/auth/logout、api/preview/access/* 免(此时前端还没有 token,但仍受第 3 层保护);其余一律走框架 Security::verify()(双提交 Cookie,token 可来自 X-CSRF-TOKEN 头、JSON 体的 csrf_token 字段或表单字段)。第 4 层覆盖的正是 AuthFilter.php:30-35 那条 Session 兜底认证路径——也就是本项要堵的洞。
【为什么这么选,备选为什么没选】
- 备选 A「所有写请求都要 token」:/AdM/ 跑的是 Vite 构建产物 public/AdM/assets/index-B4flQGaA.js(全文搜 csrf 命中 0 次),红线禁止 npm/vite 重新构建,一刀切会让现网后台全线 403;同时会打断纯 API 客户端。已在临时开严格模式时实测复现(S1 = 403)。
- 备选 B「只靠 SameSite Cookie 当主防线」:现状 session cookie 已经是 SameSite=Lax(实测 Set-Cookie: w2r_cms_session=...; HttpOnly; SameSite=Lax),能挡大部分跨站表单/fetch,但 SameSite 是按可注册域(eTLD+1)判定的——volkswagengroupchina.com.cn 下任意子域都算「同站」,被攻陷的子域照样能带上 cookie。Origin 校验比的是精确主机,正好补上这一刀。而且 Cookie.php 不在本项可改文件清单里,无法把它改成 Strict。所以 SameSite 作为既有第一道线保留,Origin+Token 作为本项新增的第二、三道线。
- 备选 C「csrfProtection 改 session 模式」:session 驱动是 FileHandler,多实例不共享,token 会随机失效。故保持 cookie(双提交)模式,服务端无状态。
- 删掉 AuthFilter 的 Session 兜底:不在本项可改文件内,且 ConfirmTokenService 和 AssetDeliveryController.php:52 依赖 session,删了会挂。本项选择「保留能力、关闭可利用性」。
【顺带修掉的坑】启用 invalidchars 后,非法 UTF-8 会让框架把原始非法字节塞进异常消息,本系统的 JSON 信封渲染再次 json_encode 失败 → 500 Fatal(实测)。所以用 InvalidCharsGuard 包了一层,收敛成 403 JSON 且不回显原始字节。另外框架在 before 过滤器短路时不跑 after 过滤器,403 响应会漏掉安全头和新 token,已在拒绝出口补齐,前端因此可以自动重试一次。
【前端】admin-frontend/js/api/index.js 增加 csrfManager:从每个响应头读 X-CSRF-TOKEN 存 sessionStorage,写请求(POST/PUT/PATCH/DELETE)自动回传,遇 code=4039 用响应里的新 token 自动重试一次。这份改动是源码,本次未构建(红线禁 vite),当前默认非严格模式下不构建也不影响现网。
修复前(实测输出)
全部在整改前配置下实测(第二组是把备份 Filters.php 换回去、等过 OPcache 2s 重校验窗口后复测的,md5 68ea94c0666a1990406d73c4aef58d2f)。
A. 会话 Cookie 属性(说明 SameSite=Lax 是唯一既有防线)
$ curl -s -D - -o /dev/null http://localhost:1977/api/auth/captcha | grep -i set-cookie
Set-Cookie: w2r_cms_session=5341fd998b456075c945ea70f7471095; path=/; HttpOnly; SameSite=Lax
B. 跨站伪造:完整登录(含 MFA)后的 cookie jar,不带 Authorization,Origin 指向攻击者站点
$ curl -s -i -b /tmp/vw_oracle.jar -X POST http://localhost:1977/api/auth/confirm-token \
-H 'Content-Type: application/json' -H 'Origin: https://evil.example.com' \
-H 'Referer: https://evil.example.com/csrf.html' \
-d '{"operation":"csrf_probe","password":"Vgc@Local2026"}'
HTTP/1.1 200 OK
Content-Security-Policy: base-uri 'self'; child-src 'self'; connect-src 'self'; default-src 'self'; font-src 'self'; form-action 'self'; img-src 'self' data:; object-src 'self'; script-src 'self' 'nonce-OMmPtbu6sqoSmqQB'; style-src 'self' 'nonce-3jT1Hj/bPPHEHXZK'; script-src-elem 'self' ...; style-src-attr 'self'
{
"code": 0,
"message": "确认token生成成功",
"data": { "token": "516bc69d0612a4f7d325575bce672d79" },
"trace_id": "cf6597827dc0da08"
}
(注意 CSP 里没有 frame-ancestors;二次确认令牌被签发给了「攻击者来源」的请求)
C. 跨站伪造 + 真实写库
$ curl -s -b /tmp/vw_oracle.jar -X POST http://localhost:1977/api/dam/categories \
-H 'Content-Type: application/json' -H 'Origin: https://evil.example.com' \
-d '{"name":"C
修复后(实测输出)
同样的请求,整改后配置(Filters.php md5 ebb6d50824b2de30844c8245a5f55a6d、Security.php md5 c50f9a0b6f60c121d2c3e4059c88c848)。
B'. 跨站伪造 + 攻击者 Origin → 被来源层拦截
$ curl -s -w "\nHTTP %{http_code}\n" -b /tmp/vw_r1.jar -X POST http://localhost:1977/api/auth/confirm-token \
-H 'Content-Type: application/json' -H 'Origin: https://evil.example.com' \
-d '{"operation":"csrf_probe","password":"Vgc@Local2026"}'
{
"code": 4030,
"message": "请求来源不可信,已拒绝(CSRF 防护)",
"data": null,
"trace_id": "e1d20638432e4f5f"
}
HTTP 403
C'. 跨站伪造 + 不发 Origin(模拟不发 Origin 的老浏览器)→ 被 token 层拦截
$ curl -s -o /dev/null -w "HTTP %{http_code}\n" -b /tmp/vw_r1.jar -X POST http://localhost:1977/api/dam/categories \
-H 'Content-Type: application/json' -d '{"name":"CSRF-SHOULD-FAIL"}'
{ "code": 4039, "message": "CSRF Token 缺失或无效,已拒绝", "data": null, "trace_id": "5be45bf4c912d5de" }
HTTP 403
(数据库复查 asset_categories 中 name like 'CSRF-%' 为空,未写入)
D'. 安全响应头(/AdM/dashboard,走 PHP 的路由)
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
Referrer-Policy: same-origin
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
CSP: frame-ancestors 'self' ← 新增(完整 CSP 其余指令不变)
E'. 非法 UTF-8 查询串 → 403 JSON(此前 200 静默接受;中间态曾是 500 Fatal,已由 InvalidCharsGuard 收敛)
{ "code": 4040, "message": "请求包含非法字符(无效编码或控制字符),已拒绝", "data": null, "trace_id": "e228fa5cdde1547a" }
HTTP 403
F'. 畸形 JSON 体(含 \x01):整改前 HTTP 500、
部署到 VW 服务器时的注意事项
1)【多实例安全,无本地状态】CSRF 走 cookie 双提交(Config\Security::$csrfProtection='cookie'),令牌哈希只存在客户端的 HttpOnly cookie w2r_csrf 里,服务端不落任何存储,因此多台 nginx+PHP-FPM、多容器副本、无粘性会话都不影响 CSRF 校验。Origin 校验的可信主机来自「请求自身的 Host 头」,同样自适应任何部署域名,不需要按环境改代码。 但要提醒:本系统 session 驱动是 CodeIgniter\Session\Handlers\FileHandler(.env: session.driver),文件 session 跨实例不共享 —— 这是既有问题、不是本次引入,多实例部署时要么开会话粘滞,要么把 session 换成 Redis/DB handler,否则 ConfirmTokenService 的二次确认码和 AssetDelivery 的 session 判定会在实例间漂移。
2)【必须由运维在 Web 层补的一处】/AdM/ 和 /AdM(后台入口首屏文档)在 backend/public 下是真实目录,被 Apache 的 DirectoryIndex 直接吐出 index.html,根本不进 PHP —— 实测该响应没有任何安全头,CI 的 secureheaders 过滤器管不到。走 PHP 的 /AdM/dashboard 等 SPA fallback 路由已经有 X-Frame-Options: SAMEORIGIN 和 CSP frame-ancestors 'self'。上线前请在站点配置里补: nginx: add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "same-origin" always; add_header Content-Security-Policy "frame-ancestors 'self'" always; Apache: Header always set X-Frame-Options "SAMEORIGIN" 等同上(需 mod_headers)。 否则点击劫持防护在真正的后台入口页上是缺失的。
3)【反向代理与 Origin 校验】Origin/Referer 比对的是请求的 Host 头(忽略 scheme,容忍 TLS 在代理侧终止)。如果 nginx 反代改写了 Host(没有 proxy_set_header Host $host),浏览器发来的 Origin 会与 PHP 看到的 Host 对不上,所有带 cookie 的写请求会 403。两条路任选:代理透传原始 Host(推荐),或在生产 .env 里写 security.trustedOrigins = 'https://cms.正式域名,https://备用域名'(逗号分隔,可带端口)。上线后请务必用真实域名跑一遍「登录 + 改一条内容」。
4)【前端必须重新构建】/AdM/ 跑的是 Vite 构建产物 backend/public/AdM/assets/index-*.js,本次只改了源码 admin-frontend/js/api/index.js,红线禁止 npm/vite 所以没有构建。当前默认(csrfStrictMode=false)下旧构建包完全可用(回归组1-3 就是按旧包的请求形态测的)。发版时请把前端重新构建发布,让 X-CSRF-TOKEN 回传真正生效。
5)【严格模式的开启条件】想让「所有带 cookie 的写请求都必须带 token」,把 Config\Security::$csrfStrictMode 置 true,或 .env 写 security.csrfStrictMode = true。前提有两个,缺一个就会挂:a) 上面第 4 条的前端重新构建已发布;b) admin-frontend/js/api/dam.js 的 uploadChunk 是自己裸写的 fetch(不走 api/index.js),必须补 X-CSRF-TOKEN 头 —— 那个文件不在本项可改清单里,需要另派。建议流程:先发前端 → 观察 operation_logs 里 module='security'、operation='csrf_token_reject' 是否为 0 → 再开严格模式。
6)【OPcache】容器实测 opcache.e
会话撤销与账号状态即时生效(登出 / 并发登录踢人 / 禁用账号 / 角色降级,
方案
核心是在 UserSessionService 里新增一个每请求执行的 checkAccess(),用一条 SQL 同时取回账号状态与会话状态,由 AuthFilter / AuthNoMfaFilter 在放行前调用;四类操作因此在下一个请求即时生效。
一条 SQL 覆盖四种失效:SELECT u.is_active, u.role, s.id, s.revoked_at FROM users u LEFT JOIN user_sessions s ON s.jti=? AND s.user_id=u.id WHERE u.id=?。users 走主键、user_sessions 走 jti 唯一索引,EXPLAIN 两表都是 const,实测 0.187 ms。判定顺序:账号不存在 → 账号被禁用 → 角色与 token claim 不符 → 会话已撤销 / 未登记。
关于「登录阶段 token 从未登记」这个陷阱,我选择的是按路由区分校验强度,不是在登录阶段补登记。理由有三条:(1) 补登记必须能区分「未完成 MFA 的半程会话」和「已认证会话」,否则 establishAuthenticatedSession 里那段并发登录检测会把用户自己的登录阶段行算成「已有活动会话」,每次正常登录都误报一条 concurrent_login_detected —— 而并发登录检测本身也是等保要交付的控制项,不能污染;(2) 要区分就得加列,加列要迁移文件,而迁移文件不在我可改文件清单里,禁令又不允许跑 php spark,直接 ALTER 会让交付树 schema 与实例漂移,部署到 VW 必然炸;(3) 登录阶段 token 的权限面只有 auth/me、mfa/status、mfa/enroll、mfa/verify 四条,本来就不承载业务权限。所以 AuthFilter(mfa_verified=true,必然由 mfaVerify 登记过)要求 jti 必须在册;AuthNoMfaFilter 允许 jti 不在册,但只要在册且已撤销就拒。
为了让「登录阶段 token 也能被撤销」,revokeSession() 改成 upsert 写「墓碑」行:jti 不在表里就直接插一条 revoked_at 非空的记录(jti 上有 UNIQUE,ON DUPLICATE KEY UPDATE revoked_at=IFNULL(revoked_at,?) 保证幂等且不覆盖首次撤销时刻)。登出因此对两个阶段的 token 都生效。另外 mfaVerify 成功后主动撤销登录阶段那枚 token —— 它是一次性步进凭证,泄露后可在有效期内重放去改 MFA 绑定。
角色降级(缺陷 3)的处理方式是「让 claim 不可能陈旧」,而不是改 requireRoles —— ApiController 不在我可改文件清单里。过滤器比对 JWT 的 role claim 与库里 users.role,不一致直接 401 让其重新登录。效果等价且更强:requireRoles 读到的 claim 只可能是当前库值,同时角色升级方向也一并收敛。mfaVerify 里签发新 token 时的角色也改成以库为准(原来读的是 JWT claim)。
拒绝一律返回 HTTP 401 + 业务码 1001,因为 admin-frontend/js/api/index.js 对 1001 的既有处理正是「补写登出审计 → 清 token → 跳登录页」,与会话失效应有的表现一致;message 按原因区分,便于用户和运维定位。每次会话级拒绝写一条 operation_logs(module=auth,operation=session_reject,result=fail,error_code=1001,request_summary 带 reason/jti/path/method),用独立 operation 与权限不足的 access_denied 区分,且不受 audit.log_access_denied 开关控制。
LogoutAuthFilter 刻意保持始终放行,只补了注释说明原因:登出是幂等收敛动作、不暴露数据,且前端收到 1001 后要靠它补写登出审计,拦住会让这条审计永远写不进来。
备选方案:(a) 把撤销状态放进缓存(Redis/文件)避免查库 —— 当前 Config/Cache.php 是 file 句柄,多实例下各写各的,撤销会按 TTL 延迟生效,与「即时」直接冲突,且实测查库代价 0.19 ms,不值得;(b) 在 JWT 里塞版本号、靠版本号失效 —— 需要改 JwtService 与签发逻辑,且无法表达单会话撤销(登出只想踢当前这一枚);(c) 改 ApiController::requireRoles 去查库 —— 文件不归我,且不能解决登出与踢人。
修复前(实测输出)
脚本 scratchpad/sess_oracle.sh,测试账号 zz_sess_probe(id=7, sys_admin, 已录 MFA),整改前输出(scratchpad/sess_oracle_before.txt,节选):
### 0. probe 完整登录
stage1_token jti=cb8462c113015ef0f2312cabec4eaf75 role_claim=sys_admin mfa=False
stage2_token jti=a59f2ee53cab157524cd85fb34fa2f4f role_claim=sys_admin mfa=True
基线 GET /api/system/session-config: HTTP=200
### 1. 登出后旧 token 应失效
POST /api/auth/logout -> HTTP=200
user_sessions.revoked_at = 2026-08-11 22:05:02 ← 撤销确实写进库了
登出后复用同一 token: HTTP=200 {"code":0,"message":"操作成功",...} ← 仍然有效
### 2. 并发登录踢人(revoked_at 已写入)后旧 token 应失效
jti=c5369dff0b9ee7f2325b43743a8564ca revoked_at=2026-08-11 14:05:03
HTTP=200 {"code":0,...} ← 踢人不生效
### 3. 禁用账号后 token 应失效
users.is_active=0
auth 路由: HTTP=200 {"code":0,...}
auth_no_mfa 路由: HTTP=200 {"code":0,"data":{"id":"7","username":"zz_sess_probe",...}} ← 禁用不生效
### 4. 角色降级后 requireRoles 应即时失效
users.role=editor / token role claim=sys_admin
HTTP=200 {"code":0,...}
### 5. MFA 完成后,登录阶段旧 token 仍可用
GET /api/auth/me: HTTP=200
补充:缺陷 3 的专项预言机(scratchpad/sess_oracle_roles.sh,把 AuthFilter.php 临时换回原始文件后跑),证明 requireRoles 与 requirePermission 强度不一致:
库内角色已降级为 editor,token 仍声称 sys_admin
[整改前] requireRoles 路径 GET /api/audit/query : HTTP=200 {"code":0,"message":"操作成功",...} ← 只信 claim,降级无效
[整改前] requirePermission 路径 GET /api/users : HTTP=400 {"code":6001,"message":"无权限访问"} ← 查库,降级已生
修复后(实测输出)
同一脚本、同一账号,整改后输出(scratchpad/sess_oracle_after.txt / sess_oracle_after2.txt):
### 1. 登出后旧 token 应失效
POST /api/auth/logout -> HTTP=200
user_sessions.revoked_at = 2026-08-11 22:10:24
登出后复用同一 token:
HTTP=401 {"code":1001,"message":"会话已失效(已登出或被新登录挤下线),请重新登录"}
### 2. 并发登录踢人后旧 token 应失效
jti=07553b553782a3c93015ee2156543f20 revoked_at=2026-08-11 14:10:25
HTTP=401 {"code":1001,"message":"会话已失效(已登出或被新登录挤下线),请重新登录"}
### 3. 禁用账号后 token 应失效
users.is_active=0
auth 路由: HTTP=401 {"code":1001,"message":"账号已被禁用,请联系管理员"}
auth_no_mfa 路由: HTTP=401 {"code":1001,"message":"账号已被禁用,请联系管理员"}
(随后恢复 is_active=1)
### 4. 角色降级后 requireRoles 应即时失效
users.role=editor / token role claim=sys_admin
HTTP=401 {"code":1001,"message":"账号角色已变更,请重新登录"}
(随后恢复 role=sys_admin)
### 5. MFA 完成后,登录阶段旧 token 已作废
GET /api/auth/me: HTTP=401 {"code":1001,"message":"会话已失效(已登出或被新登录挤下线),请重新登录"}
缺陷 3 专项(换回整改后的 AuthFilter,独立复现一次,scratchpad 内交互验证):
token role=sys_admin,库 role 降为 editor 后
GET /api/audit/query : HTTP=401 {"code":1001,"message":"账号角色已变更,请重新登录"}
GET /api/users : HTTP=401 {"code":1001,"message":"账号角色已变更,请重新登录"}
角色恢复 sys_admin 后 GET /api/audit/query : HTTP=200
审计落库(operation_logs,operation=session_reject):
628 7 sys_admin session_reject 1001 {"reason":"session_revoked","jti":"ed3b37ff...","path":"/index.php/api/system/session-config","method":"GET"}
620 7 editor session_reject 1001 {"reason":
部署到 VW 服务器时的注意事项
- 无 schema 变更、无新增迁移、无种子数据。直接覆盖这 5 个 PHP 文件即可,不需要 DBA 配合,不需要停机。回滚就是把文件换回去(备份在 scratchpad/sess_bak/*.orig,含 md5)。
- 多实例安全:判定依据全部来自 MySQL(users 与 user_sessions 两张共享表),没有引入任何依赖单机本地状态的东西。实例 A 上的登出/踢人/禁用/改角色,实例 B 下一个请求就生效。nginx + PHP-FPM 多实例、容器多副本都成立。
- 刻意不加缓存,请勿「优化」成缓存:当前 app/Config/Cache.php 的 handler 是 file,是每实例本地的;一旦给撤销状态加 TTL 缓存,其他实例上的撤销会延迟到 TTL 到期才生效,直接违背「即时生效」这条控制要求。代价已量化:EXPLAIN 两表均 const(users 主键 + user_sessions 的 jti 唯一索引),查询 0.187 ms,端到端 20 次采样 26.7 ms vs 无鉴权基线 27.3 ms,在噪声以内。若后续 QPS 真的上来了,正确做法是换共享 Redis(Config\Cache handler=redis)并把 TTL 压到 ≤5 s,同时在登出/踢人/禁用/改角色四处主动失效缓存键——不要用 file 句柄做这件事。
- 请求内备忘(static $accessMemo)在 PHP-FPM / mod_php 下天然按请求隔离;key 里带了 $_SERVER['REQUEST_TIME_FLOAT'],Swoole / RoadRunner 这类常驻 worker 也不会跨请求复用,且超过 64 条自动清空。CLI(spark)不走过滤器,不触发此逻辑。
- 上线当天的一次性影响:任何「已签发但 jti 不在 user_sessions 里」的 mfa_verified token 会被判 session_unknown 并要求重新登录。VW 全新部署不存在这类 token;若是在已有环境热更新,等于让当前在线的管理员重新登录一次,建议放在低峰。
- 与验收命令的冲突(重要):app/Commands/AuditAcceptance.php:49 与 AuditAcceptanceExtended.php:47 用 JwtService::generate(1,'sys_admin',true) 直接伪造 token、并不写 user_sessions。本次整改后这类伪造 token 访问 auth 路由会被判 session_unknown 而 401,两个验收命令里依赖 HTTP 探测的子项会失败。这两个命令本轮属红线禁跑,我没有验证,但按代码可以确定。要继续用,需要在命令里补一次 establishAuthenticatedSession 登记 jti,或改成走真实登录流程——文件不在我可改清单内。
- 时区坑:user_sessions.revoked_at 由 PHP 侧 date('Y-m-d H:i:s') 按应用时区(Asia/Shanghai)写入,而库里另有一些行是 MySQL NOW()(UTC)写的,同一列存在 8 小时差。我的判定只用 IS NULL / IS NOT NULL,不做任何时间比较,所以不受影响;后续若有人要加「撤销后 N 分钟内…」这类时间窗逻辑,必须先统一这一列的时区来源。
- PHP Session 仍是 FileHandler(Config\Session::$savePath = WRITEPATH.'session'),是每实例本地状态。二次确认码(ConfirmTokenService)、受限资产交付(AssetDeliveryController)以及 AuthFilter 的 Session 兜底鉴权都依赖它。这不是本次引入的,但多实例部署必须配置会话粘滞(ip_hash / sticky cookie)或换共享会话存储(Redis/数据库),否则这三条链路会随机失败。
- 审计写入量:每次会话级拒绝写一条 operation_logs(operation=session_reject)。触发前提是管理员做过登出/踢人/禁用/改角色,量很小;前端收到 1001 会立刻清 token 跳登录页,实测一次拒绝只产生 1-2 条。非浏览器客户端若拿着已撤销 token 死循环重试,会按请求数写入,运维侧可在 SIEM 上对 session_reject 做频次告警(这本身也是有价值的信号)。
- 建议同时确认 config 表里 auth.allow_concurrent_
MFA 密钥与备用码加密存储(AuthService.php:110-112 写
方案
两类数据分开处理,因为它们的业务需求不同。
一、TOTP 密钥 —— 必须可逆,所以做真加密。系统要用它算 TOTP、未完成注册时还要重出二维码,只能可逆加密。选的是 PHP 原生 openssl 的 AES-256-GCM,没用 CI4 自带的 CodeIgniter\Encryption\Encryption,理由有三条,都是可验证的事实而不是偏好:
- CI4 的 OpenSSLHandler 是 AES-256-CTR + HMAC-SHA512(system/Encryption/Handlers/OpenSSLHandler.php:78-110),安全但不是 AEAD,无法绑定附加数据。我们需要把密文绑到「哪一行」上:AAD = "vgc-mfa-secret|<user_id>|<provider_type>"。有了它,DBA 把 A 的密文整列搬到 B 的行上会直接认证失败(单测第 2 组已验证)。CI4 的两个 handler 都表达不了这件事。
- CI4 的密文里没有密钥标识(kid),轮换时无法判断某一行是用哪把密钥加的,只能靠「挨个试」。我们的信封格式 mfa1:<kid>:<base64(iv|tag|ct)> 自带 kid,轮换是确定性的机械操作。
- 没选 sodium:CI4 的 SodiumHandler 用 sodium_crypto_secretbox,这个 API 根本没有 AAD 参数(system/Encryption/Handlers/SodiumHandler.php:44-77),而且它会 sodium_memzero 掉传入的明文缓冲区,调用方的变量在 encrypt() 之后会变成一片 \0,是个容易踩的坑。AES-GCM 有 AES-NI 硬件加速,也是等保/密评评估人员最熟的 NIST SP 800-38D 算法。
主密钥不直接参与加密,按用途做 HKDF-SHA256 派生子密钥(info = "vgc:mfa:secret:aes-256-gcm:v1"),和备用码 pepper 的子密钥彼此隔离。
二、备用码 —— 只需校验不需读回,所以哈希存储。存法是 bcrypt(cost=10) 套在 HMAC-SHA256 pepper 之外:先 HMAC-SHA256(pepper, "<user_id>|<code>"),再 bcrypt。两层各解决一个问题:pepper 不在数据库里(跟加密密钥同源于密钥环),所以整库泄露也没法离线爆破只有 20 bit 熵的 6 位备用码;bcrypt 则是在密钥也泄露的情况下的第二道墙(等保要求鉴别信息「不可逆」,评估人员会找口令哈希算法)。HMAC 里绑了 user_id,把哈希搬到别人行上同样失效(单测已验证)。代价是 TOTP 不匹配时要跑 8 次 bcrypt ≈ 560ms,所以顺序做了调整:先算 TOTP(几次 HMAC,微秒级),不中再比备用码——正常登录一次 bcrypt 都不跑。
备用码没法再回显:getExistingMfaSecret() 现在返回 backup_codes: [] 加一个 backup_codes_remaining 计数。前端 MfaEnroll.vue:51 是 v-for,空数组不渲染也不报错(前端不在我的可改文件范围内)。
三、密钥管理放在 Config\Encryption 里做成「密钥环」,四级优先级:encryption.keyFile(推荐,K8s Secret / KMS 落盘,支持多密钥 JSON)> encryption.keys(内联 JSON 多密钥)> encryption.key(CI4 传统单密钥)> 开发兜底(仅非 production,由 JWT_SECRET 做 HKDF 派生)。生产环境缺密钥直接抛异常,不退回明文,也不自动生成——自动生成会让每个实例各拿一把,多实例下必炸。开发兜底是本地不能改 .env(红线 4)的折中,它是确定性的(同一份 .env 派生同一把密钥),多实例也一致,但已明确写死 ENVIRONMENT === 'production' 时禁用。
备选方案没选的原因:直接用 CI4 encrypter(无 AAD、无 kid,见上);把密钥写死在代码里(多实例可以,但轮换要发版,且进 git);用 writable/ 下自动生成的密钥文件(每个实例一把,多实例直接崩,明确排除)。
修复前(实测输出)
$ bash mfa_before.sh
== 1. 数据库里的原始值 (root 只读 SELECT) ==
1 1 M1lUUjIyWDIyNjYySzQyVUVQQUlQVVRQUUE= WyIxMzc0NjYiLCI0NTk3MTAiLCI0MTE3NjEiLCI5NTQwMDQiLCIxNzY1MTMiLCI1NzcwMTgiLCI1MjA3ODQiLCIyMDQxOTQiXQ==
3 6 M1lUUjIyWDIyNjYySzQyVUVQQUlQVVRQUUE= W10=
4 5 M1lUUjIyWDIyNjYySzQyVUVQQUlQVVRQUUE= W10=
== 2. 仅用数据库里的值还原明文 + 现算一枚 TOTP ==
user_id = 1
TOTP 明文密钥 = 3YTR22X22662K42UEPAIPUTPQA <-- base64_decode 即可,无需任何密钥
备用码明文 = ['137466', '459710', '411761', '954004', '176513', '577018', '520784', '204194']
现算 TOTP = 369834 <-- 可直接过 /api/auth/mfa/verify
== 3. 代码注释与事实 ==
72: $secretBase32 = base64_decode($mfaSecret->secret);
73: $backupCodes = json_decode(base64_decode($mfaSecret->backup_codes), true) ?? [];
110: // 加密存储(Base64编码) <-- 注释谎称加密
111: $encryptedSecret = base64_encode($secret);
112: $encryptedBackupCodes = base64_encode(json_encode($backupCodes));
217: $secretBase32 = base64_decode($mfaSecret->secret);
221: $backupCodes = json_decode(base64_decode($mfaSecret->backup_codes), true);
结论:一条 SELECT 就拿到全部管理员的第二因子种子和全部备用码明文;上面现算出的 369834
随后原样喂给 /api/auth/mfa/verify,返回 {"code":0,"message":"MFA验证成功"} —— 拿到库 = 完全绕过 MFA。
修复后(实测输出)
$ SELECT id,user_id,secret,LEFT(backup_codes,90) FROM vgc.mfa_secrets WHERE user_id=1\G
id: 1
user_id: 1
secret: mfa1:dev-jwt:kSiTqvbcK9Z755/jel9Zrv4OXP0mIYg4cO7S5pBMm2h7VulaH7WmtA/cJj0rp12z6Gid6K7O
backup_codes_head: {"v":1,"alg":"bcrypt(hmac-sha256)","kid":"dev-jwt","codes":["$2y$10$tJMWQbUJB7IMREYgeFqEtu
对整改前那套还原手法再试一次:
base64_decode -> 失败: Error
库里能看到的全部信息就是这串密文,没有密钥环解不出 TOTP 种子
回归套件 R1 的机器判定:
PASS 无遗留明文密钥行 (0)
PASS 无遗留明文备用码行 (0)
PASS 旧手法(base64_decode)已无法还原密钥 (UNRECOVERABLE)
加密层单测(32 项全过,摘要):
PASS 密文带版本前缀 / 密文不含明文 / 往返一致 / 两次加密密文不同(随机 IV)
PASS 换 user_id 解不开 <-- AAD 绑定,防止密文在行之间搬运
PASS 换 provider 解不开
PASS 翻转 1 bit 后解密返回 null <-- GCM 认证标签,兼顾存储完整性
PASS 信封中不含明文 137466 / 459710 / 000123
PASS 正确备用码可校验通过 / 错误备用码校验失败
PASS 同一备用码换个 user_id 校验失败(绑定生效)
PASS 过短密钥被拒 (encryption.key 的密钥长度不足(5 字节 < 16))
PASS 多密钥环下 activeKid 指向不存在的密钥被拒 (可用 kid:k1,k2)
PASS 首尾含空白/NUL 字节的二进制密钥不被 trim 破坏(keylen=32)
生产 fail-closed(独立进程,ENVIRONMENT=production):
PASS 生产环境缺密钥直接抛错: MFA 静态加密密钥未配置:请设置 encryption.keyFile 或 encryption.key(生产环境不提供任何兜底)。
PASS 生产环境配好 encryption.key 后加解密正常 (source=encryption.key)
部署到 VW 服务器时的注意事项
【一、密钥从哪来 —— 上线前必须完成,否则 production 起不来(这是故意的)】 生成:openssl rand -base64 32 四条来源按优先级,全部在 Config\Encryption 里解析,CI4 的 BaseConfig 支持用 . 或 _ 两种写法读环境变量 (encryption.key / encryption_key / Config\Encryption.key 都认,实现见 system/Config/BaseConfig.php:211-250):
- encryption.keyFile(推荐)—— 指向一个密钥文件。内容二选一:
裸密钥文本:base64:xxxxxxxx(也支持 hex2bin: 前缀或原始文本) JSON 密钥环:{"active":"vw-2026q3","keys":{"vw-2026q1":"base64:...","vw-2026q3":"base64:..."}} 文件权限 0400、属主为 PHP-FPM 运行用户。这条最适合 VW 的 KMS/凭据托管:无论是 K8s Secret 挂载、 Vault Agent sink、还是云 KMS 凭据插件落盘,最终形态都是「一个只读文件」,应用不需要认识 KMS SDK。 注意文件必须是文本,不要写原始二进制(读入时会 trim 掉首尾空白)。
- encryption.keys —— 内联 JSON 多密钥,配 encryption.activeKid。适合密钥只走环境变量、不落盘的场景。
- encryption.key —— CI4 传统单密钥。kid 默认 k1,可用 encryption.activeKid 给它命名。
- 开发兜底 —— 仅当 ENVIRONMENT !== 'production' 且存在 JWT_SECRET 时,由 JWT_SECRET 做 HKDF 派生,
kid=dev-jwt,并写 warning 日志。本地这套环境走的就是这一条(因为红线禁止改 .env)。 production 下这条被硬关:缺密钥直接抛 RuntimeException「生产环境不提供任何兜底」,不会退回明文、 也不会自动生成密钥(自动生成会让每个实例各拿一把,多实例必炸)。
【二、多实例:所有实例必须是同一把密钥】 密钥环没有任何单机本地状态(不写 writable/、不生成本地文件、不依赖机器指纹)。所有实例读同一份 keyFile / 同一组环境变量即可。nginx + PHP-FPM 形态要特别注意两点:
- PHP-FPM 默认 clear_env=yes 会清掉父进程环境变量。要么在 pool 配置里写 env[encryption.keyFile] = /etc/vgc/mfa.key,
要么设 clear_env=no,要么干脆写进 backend/.env(.env 会被 CI4 的 DotEnv 读进 $_ENV)。 只在 systemd 的 EnvironmentFile 里设、而不透传给 pool,是最常见的「本地好好的、上线读不到」。
- 改了密钥配置要 reload php-fpm;如果开了 OPcache + preload,preload.php 会缓存 Config 类,必须 reload 而不是等它自己过期。
容器化形态:keyFile 用只读 volume / Secret 挂载;不要把密钥烤进镜像层。
【三、发布顺序(多实例下这个顺序不能反)】
- 先给所有实例配好密钥(此时老代码不读它,无副作用)。
- 再把新代码发到所有实例。新代码读得懂旧的 base64 格式(encryption.mfaAllowLegacy 默认 true),
所以灰度期间新老实例共存不会互相打架。
- 全部实例都换成新代码之后,再跑一次 php spark migrate(本迁移幂等,重跑安全)。
反过来先跑迁移会让还没更新的实例读不懂新格式 —— 那一刻起没换代码的实例上谁都过不了 MFA。 代码层面也是按这个假设写的:读路径刻意不做「遗留 base64 -> 密文」的自动升级,只做 kid 轮换的自愈重封装, 就是为了不让灰度期间的某个新实例把行改成老实例读不懂的样子。
- 迁移完成、观察一两天没有 legacy 相关日志后,把 encryption.mfaAllowLegacy 设为 false,
堵死降级读取这条路(那之后往库里塞 base64 明文将不再
栏目管理权限校验(ChannelController 六个方法零鉴权)——含四方
方案
【做了什么】把「栏目管理需要哪个权限码」从代码里的硬编码变成外置配置:新建 Config\ChannelAcl 定义 read / write / reorder 三个受控动作与四套候选方案(preset),ChannelController 六个方法各加一行 guardChannel(动作) 门禁。生效优先级:config 表 > .env > 配置文件。甲方改一行 SQL 即可在四个方案之间切换,或用 channel.acl.rules(JSON)单独收紧某一个动作,均不需要改代码、不需要发版、不需要重启。
【实测的四个候选方案对比】(角色权限数据来自 role_permissions 实查) 当前七个角色实际持有:editor 8 个码(content:create/edit/delete/submit、dam:upload、figma:import、preview:create/revoke);reviewer_l1 7 个;reviewer_l2 8 个;publisher 8 个;dam_admin 3 个(仅 dam:);audit_readonly 2 个(仅 audit:read/export);sys_admin 22 个(全部)。全库共 22 个权限码,无任何 channel:。
方案 A「新增 channel:manage」:需 permissions 表 +1 行、role_permissions 授权、ChannelAcl 切 channel_manage;理想上前端 menu-permissions.js:7 也应加上该码,否则只拿 channel:manage、没有 content:* 的角色看不到栏目菜单。得失:默认只有被显式授权的角色能写,语义最干净、最符合最小权限。迁移成本:一次 DB 变更(已提供幂等 SQL 与幂等种子方法)+ 可选前端一行。实测:种码后 sys_admin 可写、editor 被拒;把该码授予 editor 角色后 editor 立刻可写(证明「改配置+改角色授权」即可放权,无需改代码)。
方案 B「复用 content:edit」:零 DB 变更、零前端变更,成本最低。但 editor、reviewer_l1、reviewer_l2、publisher 四个角色全部获得增删改栏目与拖动整棵导航树的能力(实测 policy=content_edit 时 editor read/write/reorder 全部放行)。栏目 slug 是前台 URL 的一部分,等于把改线上 URL / 断链 / 掉 SEO 的能力发给所有内容岗,不满足最小权限。
方案 C「按读写分离」(本次默认):read = content:create|content:edit|content:edit_all(与前端菜单门禁完全同一组码),write/reorder = content:edit_all(当前库中仅 sys_admin 持有)。零 DB 变更、零前端变更、当场堵住越权,且不误伤内容生产——News.vue:686 与 Content.vue:914 都要调 channelApi.getList() 取栏目下拉,读若收紧编辑器会直接不能选栏目。代价是语义耦合:content:edit_all 本意是「编辑所有内容」,被借用为「能管栏目」。
方案 D「仅 sys_admin」:零 DB 变更、最严。代价是授权变成运维动作——无法再通过「角色管理」界面把栏目维护授权给任何人;且与本系统既有设计(PermissionService 严格按 role_permissions 校验、明确不做 sys_admin 特例)相悖,等于引入角色硬编码。
【为什么默认选 C】1) 零 DB 变更即可生效,不跑种子也不会锁死任何人;2) 不误伤内容生产(编辑器栏目下拉照常);3) 立刻堵住越权——实测 audit_readonly 六个端点全 6001;4) 可平滑放权:甲方在角色管理界面给某角色 content:edit_all 即可,想要干净语义再切 channel_manage。
【防呆(已实测)】policy 写错名字、或切到 channel_manage 却忘了跑种子(权限码在 permissions 表里不存在)、或某动作解析后既无权限码也无角色——三种情况都自动回落到默认 split 并写 error 日志,不会把接口重新变成人人可改,也不会把所有人锁在门外。
【备选方案为什么没选】直接把 requirePermission('content:edit_all') 硬写进控制器:能堵洞,但把一个产品决策焊死在代码里,甲方改主意就要重新发版,且四个候选方案里没有「都同意」的安全子集,硬猜必然返工。走 config 表而不是纯 .env:VW 侧可能多实例部署,.env 要每台改且容易漂移,config 表是所有实例共享的同一份 MySQL,改一次全局一致。
修复前(实测输出)
用 audit_readonly 角色(全库只有 audit:read、audit:export 两个权限码,前端连栏目菜单都看不到)打 CMS 接口:
### [1] GET /api/channels as audit_readonly (无任何 content 权限)
code=0 msg=操作成功 items=1
### [2] POST /api/channels as audit_readonly
{"code": 0, "message": "创建成功", "data": {"id": 4}, "trace_id": "0b67b82e62b4484a"}
### [3] PUT /api/channels/1 改线上栏目 slug(SEO 影响)as audit_readonly
{"code": 0, "message": "更新成功", "data": null, "trace_id": "34a8ad8a0aab49ef"}
id slug_en
1 zz-hijacked
### [4] PUT /api/channels/reorder 把线上 news 挂到假栏目下 as audit_readonly
{"code": 0, "message": "排序已保存", "data": {"ok": true, "updated": 1}, "trace_id": "1f2937b0c088d62a"}
id parent_id sort_order slug_en
1 4 9 zz-hijacked
4 NULL 0 zz-pentest
### [5] DELETE /api/channels/4 as audit_readonly
{"code": 0, "message": "删除成功", "data": null, "trace_id": "d680d388786f1c9f"}
### 官网 /api/menu 受影响实证(整改前)—— 只读审计账号改掉了官网导航
{
"success": true,
"data": [
{"id": 4, "name": "ZZ越权测试栏目", "slug": "zz-pentest", "href": "/zz-pentest", "sort_order": 0,
"children": [{"id": 1, "name": "新闻中心", "slug": "news", "href": "/news", "sort_order": 9, "children": []}]}
]
}
修复后(实测输出)
同一账号、同一批请求(交付默认态 split,config 表无 channel.acl.* 行):
audit_readonly GET /api/channels -> code=6001 无权限操作栏目
audit_readonly GET /api/channels/1 -> code=6001 无权限操作栏目
audit_readonly POST /api/channels -> code=6001 无权限操作栏目
audit_readonly PUT /api/channels/1 -> code=6001 无权限操作栏目
audit_readonly PUT /api/channels/reorder -> code=6001 无权限操作栏目
audit_readonly DELETE /api/channels/1 -> code=6001 无权限操作栏目
(HTTP 400 + 业务码 6001;DB 中 channels 表零改动)
审计留痕(operation_logs,六次拒绝逐条落库,且记下了「要什么权限才够」):
283 access_denied 6 audit_readonly fail 6001 {"permission":"channel:write(content:edit_all)","path":"\/index.php\/api\/channels\/1","method":"DELETE"}
282 access_denied 6 audit_readonly fail 6001 {"permission":"channel:reorder(content:edit_all)","path":"\/index.php\/api\/channels\/reorder","method":"PUT"}
281 access_denied 6 audit_readonly fail 6001 {"permission":"channel:write(content:edit_all)","path":"\/index.php\/api\/channels\/1","method":"PUT"}
280 access_denied 6 audit_readonly fail 6001 {"permission":"channel:write(content:edit_all)","path":"\/index.php\/api\/channels","method":"POST"}
279 access_denied 6 audit_readonly fail 6001 {"permission":"channel:write(content:edit_all)","path":"\/index.php\/api\/channels","method":"POST"}
278 access_denied 6 audit_readonly fail 6001 {"permission":"channel:read(content:create|content:edit|content:edit_all)","path":"\/index.php\/api\/channe
部署到 VW 服务器时的注意事项
- 无单机本地状态依赖。策略读 config 表(所有实例共享同一 MySQL),优先级 config 表 > .env > PHP 配置文件。多实例(nginx+PHP-FPM 集群或多容器)只需改一次 config 表,全实例立即一致,不需要逐台改 .env,不需要重启、不需要 reload PHP-FPM。缓存只在单次请求内(Config 类的 static,请求结束即释放),不存在实例间脏读窗口,唯一延迟来源是 MySQL 主从复制。
- 切换方案的标准运维动作(三选一,推荐第一种):
INSERT INTO config (key,value,description,created_at,updated_at) VALUES ('channel.acl.policy','sys_admin_only','栏目管理鉴权方案',NOW(),NOW()) ON DUPLICATE KEY UPDATE value=VALUES(value), updated_at=NOW(); 单动作收紧:键 channel.acl.rules,值形如 {"reorder":{"permissions":[],"roles":["sys_admin"]}} 或改 .env:channelacl.policy = sys_admin_only(多实例需逐台改,不推荐) 或改 app/Config/ChannelAcl.php 的 $policy(需发版;若线上开了 opcache,改 PHP 文件必须 reload php-fpm 才生效,走 config 表则不必)。
- 启用 channel:manage 这条路(可选,需要 DBA 配合执行一次):
幂等 SQL 已完整写在 app/Config/ChannelAcl.php 文件头注释第六节,也等价于 \App\Database\Seeds\RolePermissionSeeder::ensureChannelManagePermission()(纯 INSERT、可反复执行)。 执行顺序必须是「先种权限码,再改 config 的 policy」;顺序反了不会出事——本实现会检测到权限码不存在并自动回落到 split,只在日志里报 error。 建议同步让前端把 admin-frontend/js/config/menu-permissions.js:7 与 js/router/index.js:36 的 requiresPermission 加上 'channel:manage',否则只授 channel:manage、不授 content:* 的角色看不到栏目菜单入口。
- 严禁在已上线环境执行 php spark db:seed App\Database\Seeds\RolePermissionSeeder —— 该 run() 会先 DELETE channel_role_permissions / role_permissions / permissions / roles 再重建,等于清空全部授权。已在该方法上加了【危险】注释,线上新增权限码请一律走 ensureChannelManagePermission() 或等价 SQL。
- 性能:每个栏目请求新增最多 3 次轻量查询(config 两键 + permissions 全表 22 行)加 1 次用户权限查询,且同一请求内只查一次。若 VW 侧要求进一步压减,把 ChannelAcl 的 $allowDbOverride 设为 false,退化为纯文件配置(届时切换方案需发版)。
- 配置基线锁定:等保配置管理若要求「策略不得运行时变更」,把 $allowDbOverride = false 即可,此时 config 表里的 channel.acl.* 一律被忽略,只认发版内容。
- 观察到但不属于本任务:config 表原有的 auth.allow_concurrent_sessions(原 id=2)在本次会话期间被移除,不是本任务所为(本任务只 INSERT/DELETE 过 channel.acl.policy 与 channel.acl.rules 两行,均已清理)。同期还有并行代理创建的 sys_admin 账号 zz_sess_probe(users.id=7)残留,未动。上线前请并入总检查清单确认。
等保控制项对照与定级建议(只读分析,不改代码)— VW 集团(中国)官网 CMS
方案
一、定级建议:应定三级,文档写的「等保二级导向」偏低
结论:/Users/linhuasun/vwcorp-local/w2r.site/docs/project-introduction.md:25 写「系统设计遵循等保二级导向」,我认为定级偏低一档,建议按三级定级备案;若甲方坚持二级,最低限度也要「备案二级、建设对齐三级」。
定级依据是 GB/T 22240-2020《网络安全等级保护定级指南》的两个定级要素:受侵害的客体、对客体的侵害程度。
支持二级的论据(我实测确认成立,但不足以定二级):
- 不处理公民个人信息 —— 实证:官网前台
demo.w2r.site/frontend/src下<input>元素 0 个,无表单、无留资、无线索采集;vgc.users表字段仅 username / email / password_hash / role / is_active / 三个时间戳,是内部管理员账号,不是公民个人信息。这是二级论据里唯一站得住的一条。 - 大概率不属于关键信息基础设施 —— CII 认定由行业主管部门按《关键信息基础设施安全保护条例》做,汽车制造虽属「重要行业和领域」,但对外宣传官网通常不进 CII 清单。
支持三级的论据(更重):
- 受侵害客体不止「法人合法权益」。大众汽车集团(中国)是在华合资体量最大的外资车企之一,官网是其唯一官方对外发布渠道。CMS 的核心风险就是「改内容」——首页/新闻被篡改后发布伪造的召回公告、安全声明或官方立场,会被媒体即时放大,可能引发消费者恐慌、经销商体系混乱、母公司股价波动。这已经越过「仅损害法人权益」,落到「损害社会秩序和公共利益」。
- 侵害程度是「严重损害」而非「一般损害」。按定级矩阵:客体=社会秩序和公共利益 + 程度=一般损害 → 二级;+ 严重损害 → 三级。汽车行业头部品牌官网的内容篡改事件,其社会影响面难以论证为「一般」。
- 行业实践:主机厂面向公众的门户/官网普遍按三级备案,这是专家评审会的默认预期。定二级需要额外举证,反而增加评审不通过的风险。
- 系统自己的实现已经超过二级。
Services/AuditAnomalyService.php做了访问拒绝阈值告警和非工作时间查询频率告警——这是三级 8.1.5.2 审计管理 / 8.1.5.4 集中管控 才要求的能力,二级不要求。系统实际按三级思路建的,文档却写二级,这个不一致本身在评审时会被追问。
定二级 vs 三级的实际差异(成本在哪): 定二级则 8.1.4.8 数据保密性、8.1.5.3 安全管理、8.1.5.4 集中管控 三组整体不进测评范围。其中 8.1.5.4 集中管控要求日志集中收集、安全设备集中监控、独立管理区域,需要采购 SIEM/日志审计平台,是二级三级之间最大的一笔预算差。8.1.4.8 数据保密性正好命中本系统当前最严重的缺陷(MFA 密钥明文存储)。也就是说,定二级会「掩盖」掉当前最高风险的那一项——这是我建议定三级的另一个理由:定级不该被用来规避已知缺陷。
最终定级不是技术侧能拍板的,须由甲方组织专家评审并经公安机关备案审核(见 decisions_needed 第 1 条)。
标准条款编号已核实:GB/T 22239-2019 第 7 章为第二级安全要求、第 8 章为第三级安全要求;8.1.4.1 身份鉴别 / 8.1.4.2 访问控制 / 8.1.4.3 安全审计 / 8.1.4.11 个人信息保护 编号经检索确认(检索时间 2026-08-11)。来源:https://www.cnblogs.com/hemukg/p/18817035 、https://www.ahdjbh.com/dengbaoceping/353.html 、https://safe.cafe/docs/djbh/19/ 、https://www.freebuf.com/articles/es/218259.html
二、控制项对照表
格式:条款号 名称 | 是否满足 | 实现在哪 | 差距 | 整改建议。文件路径均相对 /Users/linhuasun/vwcorp-local/w2r.site/backend/(前端相对 w2r.site/ 或 demo.w2r.site/)。
A. 安全通信网络 8.1.2(二级 7.1.2)
8.1.2.1 网络架构 | 不适用于应用层 | — | 网络分区、带宽保障、关键线路与设备冗余属基础设施 | 甲方 IT 提供网络拓扑与冗余证明。
8.1.2.2 通信传输 | 不满足(应用层) | app/Config/App.php:19 baseURL='https://w2r.site/' 声明了 HTTPS | app/Config/App.php:160 forceGlobalSecureRequests=false;app/Config/Cookie.php:57 secure=false(实测响应 Set-Cookie 无 Secure 属性);响应头无 Strict-Transport-Security。TLS 若在 nginx 终结,应用层完全不设防,一次 http 回退就明文外泄 JWT 与 session id | 开 forcehttps + Cookie secure=true + HSTS;同时配 app/Config/App.php:183 $proxyIPs(当前为空数组)并让 nginx 传 X-Forwarded-Proto,否则会重定向死循环。
8.1.2.3 可信验证 | 不满足 | — | 无可信根、无启动度量 | 纯应用系统普遍拿不到分,走补偿措施 + 风险接受。
B. 安全区域边界 8.1.3(二级 7.1
修复前(实测输出)
本项是只读评估,「整改前」= 我实测到的当前不合规实证。以下输出原样贴。
═══ PROBE B6b — 8.1.4.4 d) 全站无 CSRF:跨站表单 POST 仅凭 session cookie 即写库 ═══
$ curl -s -b jar -X POST http://localhost:1977/api/dam/categories \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'Origin: https://evil.example.com' -H 'Referer: https://evil.example.com/' \
--data-urlencode 'name=ORACLE_CSRF_PROOF'
{
"code": 0,
"message": "创建成功",
"data": {
"id": "7",
"name": "ORACLE_CSRF_PROOF",
"access_level": "public",
"created_by": "1",
"created_at": {"date": "2026-08-11 21:59:43.000000",
--- DB 验证 ---
7 ORACLE_CSRF_PROOF 2026-08-11 21:59:43
--- 该请求被审计成了管理员本人(抗抵赖被破坏,8.1.4.3)---
229 1 dam category_create 7 172.20.0.1 2026-08-11 21:59:43
(无 Bearer 头、无 CSRF token、Origin/Referer 均为攻击者域,请求依然通过鉴权与授权并落库)
═══ PROBE B2/B4 — 附带:500 错误外泄完整堆栈与服务器绝对路径(8.1.4.4)═══
{
"title": "CodeIgniter\\Exceptions\\InvalidArgumentException",
"code": 500,
"message": "Field \"start_at\" is not nullable, but null was passed.",
"file": "/www/wwwroot/w2r.site/backend/system/DataCaster/DataCaster.php",
"line": 156,
"trace": [ ...
═══ PROBE G — 8.1.4.1 b) / 8.1.4.10 b) 登出不吊销 JWT ═══
before logout: GET /api/users -> HTTP 200
logout -> HTTP 200
AFTER logout, SAME token: GET /api/users -> HTTP 200
{"code": 0, "message": "操作成功", "data": {"list": [{"id": "6", "username": "zz_sec_audit", ..
修复后(实测输出)
**本项是只读评估任务,按设计 oracle_after ≡ oracle_before —— 我没有改任何代码,因此上述每一条不合规在本报告出具后依然成立。** 这不是遗漏,是任务边界(任务书明确「本任务只做分析,不修改任何文件」)。
「整改后」在这里的正确含义是:确认评估过程本身没有破坏系统、没有留下残留。以下是验证输出。
═══ 系统完好性(评估未造成副作用)═══
$ curl -o /dev/null -w '%{http_code}' /api/auth/me -H Bearer -> 200
$ curl -o /dev/null -w '%{http_code}' /api/users -H Bearer -> 200
$ curl -o /dev/null -w '%{http_code}' /api/users 无凭证 -> 401
$ curl -o /dev/null -w '%{http_code}' http://localhost:1978/ -> 200 size=406
═══ 取证数据已清理,零残留 ═══
$ curl -X DELETE /api/dam/categories/7 -H Bearer
{"code": 0, "message": "删除成功"}
SELECT COUNT(*) FROM vgc.asset_categories WHERE name LIKE 'ORACLE%'; -> 0
SELECT COUNT(*) FROM vgc.search_hotwords WHERE keyword LIKE 'ORACLE%';-> 0
SELECT COUNT(*) FROM vgc.channels WHERE name LIKE 'ZZ_MLPS%'; -> 0
(创建与删除两笔均正常留痕:operation_logs id 229 category_create / id 230 category_delete)
═══ 工作副本未被本会话写入 ═══
$ stat -f '%Sm %N' app/Config/Filters.php -> 2026-05-21 14:12:14
$ stat -f '%Sm %N' app/Config/Security.php -> 2026-02-08 20:32:20
(这两个文件在我全程读取期间 mtime 未变,与我 files_changed=[] 一致)
═══ 并行整改进度的旁证(供协调用,非我方改动)═══
$ stat -f '%Sm %N' app/Filters/AuthFilter.php -> 2026-08-11 22:08:06
$ stat -f '%Sm %N' app/Services/AuthService.php -> 2026-08-11 22:10:16
$ ls app/Config/ChannelAcl.php -> 存在(新增文件)
说明:我的全部取证完成于 21:55–22:02,晚于取证时间的这几次改动是并行代理的会话撤
部署到 VW 服务器时的注意事项
部署到 VW 官方服务器(nginx + PHP-FPM 或容器化,且很可能多实例)时的注意事项。多实例相关的前四条是硬阻塞,不解决会出现随机性故障,而且会让若干等保控制项名存实亡。
1. Session 用文件存储,多实例必坏(硬阻塞) app/Config/Session.php:24 driver=FileHandler、:64 savePath=WRITEPATH.'session',实测本地已堆积 38 个文件。多实例/多容器下会话不共享,三条链路会随机失败:二次确认(app/Services/ConfirmTokenService.php:30 把确认码存 PHP session)、受限资产交付(app/Controllers/AssetDeliveryController.php:52 先读 session)、AuthFilter 的 session 回退(app/Filters/AuthFilter.php:31-35)。必须换 Redis 或 DB session。nginx sticky session 是下策——滚动发布时仍会掉,而且掩盖问题。
2. Cache 用文件存储,登录限流形同虚设(硬阻塞,且直接影响 8.1.4.1 b) app/Config/Cache.php:24 handler='file'。app/Services/LoginThrottleService.php 的失败计数落本地磁盘 → N 个实例意味着攻击者拿到 5×N 次尝试,且锁定只锁住其中一个实例。验证码同理(缓存文件 writable/cache/captcha_*)。测评时「登录失败处理」这一项会因此判不符合。必须换 Redis。
3. 加密密钥是 MFA 整改的硬依赖,必须写进部署清单(需运维配合) app/Config/Encryption.php:24 key='',且 .env 里没有 encryption.key(实测 grep 计数 0)。MFA 加密整改必然要一把密钥,部署时需要:生成 32 字节随机密钥 → 以环境变量注入 → 保证所有实例一致 → 纳入甲方密钥管理流程(KMS 或密码机)→ 明确轮换周期。特别注意轮换方案:轮换时必须能同时用新旧钥解密以重加密存量 mfa_secrets,否则一轮换全员 MFA 失效、且没有恢复路径。这是整个整改里唯一必须运维强配合、且做错不可逆的一项。
4. JWT_SECRET 的注入方式 app/Config/Jwt.php:54 已支持从 $_ENV['JWT_SECRET'] 读取(实测 .env 里有),但 :58-68 有一段「自己 file() 读 .env 文本」的兜底——容器化下这段可能读不到,而且它把密钥读取从框架配置里旁路了。部署时请确保走真正的环境变量,不要依赖这段兜底。所有实例的 secret 必须一致;轮换会一次性踢掉全部在线用户,需与甲方约定窗口。
5. 反向代理下的真实 IP(影响审计准确性与限流有效性) app/Config/App.php:183 $proxyIPs=[]。nginx 前置时 getIPAddress() 拿到的是负载均衡器 IP,审计日志的 ip 字段与限流的 key 全部失真——8.1.4.3 b) 审计记录要求「用户」可追溯,8.1.4.1 b) 限流要求按来源限制,两项都会被打掉。本地实测审计日志 IP 全是 172.20.0.1(Docker 网关),就是这个问题的预演。部署时必须配 $proxyIPs 并让 nginx 传 X-Forwarded-For / X-Forwarded-Proto。
6. HTTPS 强制的正确打开方式 app/Config/App.php:160 forceGlobalSecureRequests=false。若 TLS 在 nginx 终结、应用只收 http,直接开 forcehttps 会导致重定向死循环。正确顺序是:先配好第 5 条的 $proxyIPs 与 X-Forwarded-Proto,再开 forcehttps,同时把 app/Config/Cookie.php:57 secure 改 true,并在 nginx 加 HSTS 响应头。
7. nginx / PHP-FPM 层必须做的加固
- 上传目录禁止 PHP 执行。DAM 白名单允许 zip/doc/docx(
app/Config/Dam.php:39),一旦白名单被绕过而目录又能跑 PHP,就是 RCE。这条是 8.1.4.5
完整调查报告(4 份,156 项发现)
未交付清单(穷举)——大众汽车集团(中国)官网 CMS 与官网前台交接(65 项)
未交付清单(穷举)
排查范围:/Volumes/ProjectsAPFS/VWCORP/ 下 w2r.site.zip(241 MB)与 demo.w2r.site.zip(101 MB)解包后的全部内容,外加两份压缩包本身的条目清单(unzip -l)。路径一律相对交付根目录。「事实」= 我在文件里直接查到;「推断」= 我从证据推出、需要人确认。
结论先说:这份交付可以读懂代码,但不能重建系统。 缺的不是零星文件,而是三整类东西——(1) 全部业务数据与媒体实体,(2) 全部部署与环境配置,(3) 官网前台 demo.w2r.site 从 2026-04 到 2026-06 的三个月增量代码。第 (3) 项此前未被识别,是本次排查最重的发现。
A. 数据与运行时资产(最致命,缺了系统起不来)
| # | 缺失项 | 证据 | 影响 | 谁持有 | |||
|---|---|---|---|---|---|---|---|
| A1 | 数据库导出(结构 + 数据) | 全交付树 find . -iname ".sql" 结果为空;.dump / *.sql.gz 同样为空 | 内容、栏目树、用户与 MFA 绑定、审计日志、DAM 元数据、config 表全部为零。表结构可由 w2r.site/backend/app/Database/Migrations/(51 个迁移)+ app/Database/Seeds/RolePermissionSeeder.php 重建,数据不能 | 47.242.46.253 上的 MySQL 5.7.44(版本见 w2r.site/docs/software-bom.md:20) | |||
| A2 | w2r.site/backend/writable/ 整个目录 | `unzip -l w2r.site.zip \ | grep -c "w2r.site/backend/writable/" = 0;本地 ls w2r.site/backend/writable` 报 No such file or directory | 三重后果:① DAM 全部物理文件(uploads/dam/YYYY/MM/)不存在;② Figma 导入产出的页面样式 uploads/page-css/ 不存在,前台所有 Figma 页面会裸奔;③ CI4 缺 `writable/cache | logs | session` 连启动都会报错 | 同上服务器 |
| A3 | config 表业务配置数据 | docs/cms/config-promotion.md:13-21 列出 8 类必须可导入导出的配置;迁移里只有 2025-01-19-100003_CreateConfigTable.php 建空表 + 2026-03-12-100042_SeedSitecoreChannelToCategoryConfig.php 播一项 Sitecore 映射 | 工作流规则(workflow.rules)、SEO 站点级配置、会话与审计策略、搜索热词/置顶全部丢失,只能人工重配 | 数据库 | |||
| A4 | 迁移脚本的中间缓存 | docs/import_vgc_news.md:15 要求 thumb_items.json;scripts/README.md 的 Sitecore 新闻流程依赖 writable/import_sitecore_news/list_zh.json、list_en.json、details.json。实交付 w2r.site/writable/import_sitecore_news/ 只有 newslist.json、get_news_raw_sample.json | 断点续传缓存不全,重跑导入要从零抓取外站,且外站结构可能已变 | 服务器 writable/ | |||
| A5 | Sitecore 侧源数据导出 | docs/sitecore-files-import-plan.md、docs/cms/sitecore-dam-migration-analysis.md 全篇以「从 Sitecore 在线抓取」为前提;backend/.env:16-19 存的是 Sitecore 登录地址与账号(值不摘录,见该文件) | 一旦旧 Sitecore(211.151.111.91)下线,历史内容与媒体再也拉不回来 | 现有官网 Sitecore 运维方 |
B. 版本控制:三个仓库都不完整,一个站点根本没进仓库
| # | 缺失项 | 证据 | 影响 | 谁持有 | |
|---|---|---|---|---|---|
| B1 | GitLab 三仓库的访问权 | w2r.site/backend/.git remote = https://gitlab.onedevops.vw.com.cn/c-sdxx/corporate-website/backend.git;admin-frontend/.git remote = .../frontend.git;w2r.site/.git_root_backup_20260521_174151 remote = .../corporate-website.git | 没有仓库权限就拿不到完整提交历史、分支、MR 记录、SAST 扫描历史 | 大众内网 GitLab(gitlab.onedevops.vw.com.cn),需甲方 IT 开通 | |
| B2 | demo.w2r.site 完全不在版本控制内 | `unzip -l demo.w2r.site.zip \ | grep -c "/\.git/" = 0;ls demo.w2r.site/frontend/.git 与 backend/.git 均不存在;demo.w2r.site/.gitignore` 只有 2 行且只忽略一个编译标记 | 官网前台没有任何历史版本、没有回滚点、没有 CI。改坏了只能靠这份 zip | 无人持有——历史不存在 |
| B3 | 两个仓库的未提交改动 | w2r.site/backend git status --porcelain = 92 项,最后一次提交 37906d2 日期 2026-05-26;admin-frontend = 16 项,含 5 个从未入库的新文件(js/api/preview.js、js/api/publish.js、js/composables/useContent*.js、js/utils/contentPreview.js) | GitLab 上的代码不等于交付的代码,也不等于线上代码。三者之间的差异无人能核对 | 交付方本地 / 服务器工作树 | |
| B4 | docs/ 与 scripts/ 被 gitignore 排除 | w2r.site/.gitignore:5 = docs/,:7 = scripts/;根仓库 git ls-tree HEAD --name-only 只有 .gitignore .gitlab-ci.yml .user.ini README.md admin-frontend backend | 31 份文档与 12 个迁移脚本从未进过任何仓库,只存在于这份 zip 和服务器磁盘上。一旦 zip 丢失即彻底丢失 | 仅此 zip | |
| B5 | 根仓库是备份目录而非活动仓库 | 目录名 w2r.site/.git_root_backup_20260521_174151,最后提交 32f1ab54 Update files | 根级 git status 无法直接跑,交接方拿到的不是可用工作树 | — |
C. .cursor 规范与知识体系(文档第四章有完整清单,实交付近乎为零)
| # | 缺失项 | 证据 | 影响 | 谁持有 | |
|---|---|---|---|---|---|
| C1 | w2r.site/.cursor/rules/ 15 条 rules | 清单见 docs/project-introduction.md:268-284(001/005/010/020/030/040/045/050/060/070/080/090/095/096/099);`unzip -l w2r.site.zip \ | grep -c "\.cursor" = 0;w2r.site/.gitignore:1 明确排除 .cursor/` | 分层约定、错误码分段(1xxx–9xxx)、审计与 trace_id 规范、迁移优先原则、文档同步要求全部只剩标题 | 服务器 /www/wwwroot/w2r.site/.cursor/ |
| C2 | w2r.site/.cursor/skills/ 10 个 skills | 清单见 docs/project-introduction.md:288-299;docs/cms/security-model.md 等处引用 .cursor/skills/cms-security-audit/SKILL.md,交付树中不存在 | 同上 | 同上 | |
| C3 | workspace 级 /www/wwwroot/.cursor/rules/ | demo.w2r.site/frontend/README.md:14 明写「Workspace 级 URL 语言约定 见 /www/wwwroot/.cursor/rules/」;demo.w2r.site/.cursor/rules/media-permissions-mfa.mdc:13 明写「完整规则见 workspace:.cursor/rules/005-demo-media-permissions-mfa.mdc」 | 中英文 URL 约定、媒体权限与 MFA 的完整规则不在交付内;实交付只有 2 条摘要级 rules | 服务器 /www/wwwroot/.cursor/ |
D. 文档:workspace 一级目录整个缺席,且交付内文档自相矛盾
| # | 缺失项 | 证据 | 影响 | 谁持有 |
|---|---|---|---|---|
| D1 | workspace 级 docs/changelog.md | w2r.site/docs/changelog.md:87、:168 两处「见 workspace docs/changelog.md」,用于解释「已发布内容通过修订稿编辑」与 page-css 404 根因 | 关键变更的完整说明缺失,只剩指针 | 服务器 /www/wwwroot/docs/ |
| D2 | workspace docs/demo-video-media-delivery.md | w2r.site/docs/changelog.md:237 与 docs/cms/video-delivery-and-transcoding.md:4(相对路径 ../../../docs/)均指向它,内容为「批量队列下载」前台交互 | 视频 720p/1080p 交付与批量下载的前台实现说明缺失 | 同上 |
| D3 | workspace docs/demo-media-permissions-mfa.md | w2r.site/docs/changelog.md:266 | 媒体账号注册 → 后台审批 → 激活 → MFA 的完整规则缺失 | 同上 |
| D4 | workspace docs/figma-designer-checklist.md | docs/cms/figma-import.md:5「设计侧交付规范(图层命名、Auto Layout、导入不支持的效果)见 Workspace…」 | 设计师如何出稿才能被 Figma 导入正确解析——这条链路的规范没了,后续设计返工风险高 | 同上 |
| D5 | docs/checklist.xlsx | docs/cms/checklist-implementation.md:3、:48、:53 反复引用,全表 53 行是客户方的安全检查表 | 合规自查的原始表格缺失,checklist-implementation.md 的 B/C 列无处可贴 | 甲方或交付方 |
| D6 | docs/checklist-mapping.csv | docs/cms/checklist-implementation.md:4、:49 | 同上 | 同上 |
| D7 | 变更记录停在 2026-06-21,代码到 2026-07-22 | docs/changelog.md 最新条目为 ## 2026-06-21;w2r.site/backend/app/Controllers/Api/ContentController.php 与 admin-frontend/js/views/Content.vue 文件时间为 2026-07-22 09:30 | 最后一个月的改动没有任何书面记录。(推断:时间戳成批集中在 07-20 17:51 与 07-22 09:30,可能是打包时的批量拷贝所致,需人确认这一个月是否真有代码变更) | 交付方 |
| D8 | 项目 README(安装 / 初始化 / 启动) | w2r.site/README.md 是 GitLab 默认模板全文(「Choose a self-explaining name for your project」原样保留);w2r.site/backend/README.md 是 CodeIgniter 4 官方发行版 README 原文 | 新人拿到代码没有任何入门路径 | 无人写过 |
| D9 | 部署 / 运维 / 备份恢复 / 回滚手册 | 全树无 runbook 类文档;docs/ 31 份全是设计与迁移说明 | 上线、扩容、故障恢复全靠口口相传 | 交付方 |
| D10 | 机器可读接口规范 | find 无 openapi、swagger、postman。接口文档只有两个二进制 Word:docs/Interface-Agreement.docx(38.9 KB,推测为接口约定)、docs/Interface-List.docx(39.7 KB,推测为接口清单),且无 Markdown 源稿(其余 .docx 均有同名 .md) | 接口无法自动校验、无法生成客户端、无法做契约测试;Word 一旦与代码不符没人发现 | 交付方(生成脚本亦缺失,见 E4) |
| D11 | dev-plan.md 与实现严重脱节 | docs/cms/dev-plan.md:107(阶段 4 工作流)、:132(阶段 5 发布预览)、:164(阶段 6 DAM)、:197(阶段 7 搜索)、:229(阶段 8 配置)全标「⏳ 待实现」,:315 写「当前进度:已完成阶段 1-3」。但 backend/app/Config/Routes.php:117-171 这些接口全部存在 | 文档不能当验收依据;接手方按文档判断进度会严重误判 | 交付方 |
E. Python / PHP 工具脚本:README 里写了 12 个,实交付 12 个但对不上
w2r.site/scripts/ 实际有 12 个 .py,但其中 5 个 README 里描述过的关键脚本不存在,另有 3 个存在但 README 没写。
| # | 缺失项 | 证据 | 影响 | 谁持有 |
|---|---|---|---|---|
| E1 | scripts/import_vgc_news.py | scripts/README.md:181 有完整章节;docs/import_vgc_news.md:4 指名此脚本。目录里只剩它的依赖文件 scripts/requirements-import_vgc_news.txt | 官网新闻缩略图导入 DAM 的主脚本没了,asset_entry_i18n 关联无法重建 | 交付方 |
| E2 | scripts/import_mediacenter_news.py | scripts/README.md:252、:274、:281-282 | 媒体中心新闻导入不可复现 | 交付方 |
| E3 | scripts/md_to_docx.py | docs/README.md:8「由上列 Markdown 经 scripts/md_to_docx.py 生成」;scripts/README.md:293、:308 | project-introduction.docx 无法从 Markdown 重新生成,Word 与 Markdown 会永久漂移 | 交付方 |
| E4 | scripts/gen_interface_docs.py | scripts/README.md:315、:340(用 .venv-docx/bin/python 跑) | D10 的两份接口 Word 无法重新生成 | 交付方 |
| E5 | scripts/stats_sitecore_files_import.py | docs/import_sitecore_files.md:86、:89、:92 | Sitecore 文件导入的结果统计无法复核 | 交付方 |
| E6 | scripts/migrate_config_v1_to_v2.php | docs/cms/config-promotion.md:193 以代码注释形式给出路径 | 配置版本升级迁移路径缺失 | 交付方 |
| E7 | 统一的 requirements.txt | 全树仅 scripts/requirements-import_vgc_news.txt(275 B);其余 11 个脚本的依赖靠 README 正文口述(pymysql、bcrypt、requests、playwright、python-docx) | Python 环境不可复现 | 交付方 |
| E8 | 文档生成用的两个虚拟环境是空壳 | w2r.site/.venv-doc/ 与 .venv-docx/ 内只有 include/python3.12,无 bin、无 lib;.venv/lib/python3.12/site-packages/ 只有 bcrypt / pymysql / requests / certifi / urllib3 等,无 docx、无 playwright,且 .so 为 x86_64-linux-gnu(服务器 venv,本机不可用) | 文档生成链路(E3 + E4)即使脚本找回来也跑不了 | 交付方 |
F. 部署与环境:这套代码不知道该怎么上线
| # | 缺失项 | 证据 | 影响 | 谁持有 |
|---|---|---|---|---|
| F1 | 真实站点配置(nginx / 宝塔) | 全树无 *.conf、无 vhost、无 Dockerfile(localdev/ 是接手方自建,不算交付物)。交付的改写规则全在 Apache 语法的 w2r.site/.htaccess、backend/public/.htaccess、demo.w2r.site/backend/public/.htaccess、demo.w2r.site/frontend/public/.htaccess 里;但 backend/public/404.html 是 nginx 默认错误页、public/index.html 是宝塔面板页 | nginx 不读 .htaccess。线上所有路由改写(/AdM/ SPA 回退、/api/* 转 api.php、/asset/{id} 302、SPA history 回退)都靠一份未交付的配置在撑。换机重建必然 404 满地 | 47.242.46.253 的宝塔面板 |
| F2 | 定时任务配置(cron) | docs/cms/acceptance-checklist.md:18 要求 php spark publish:run-schedule 可用,backend/app/Commands/RunPublishSchedule.php、CleanAuditLogs.php、CleanApplicationLogs.php 均存在;但全树无 crontab、无 systemd timer | 定时发布/下线、审计日志 6 个月清理不会自动跑。(推断:线上应有 crontab,需从服务器导出) | 服务器 |
| F3 | TLS 证书与续期机制 | demo.w2r.site/.well-known/acme-challenge/nk2ypvpch9dpho1xjfyozelzg70mzqma 表明用 ACME(Let's Encrypt / 宝塔)签发;证书与私钥不在交付内(这点是对的,但必须走单独渠道交接) | 证书到期无人续 | 服务器管理员 |
| F4 | 生产环境 .env | 交付的 w2r.site/backend/.env:5 为 CI_ENVIRONMENT = development;backend/app/Config/App.php:19 baseURL = 'https://w2r.site/',而 docs/project-introduction.md:44 称生产域名是 https://vgc.digirepub.com/;app/Config/App.php:160 forceGlobalSecureRequests = false | 交付的是开发档配置。生产的密钥、JWT 密钥、Figma token、数据库口令均需另行交接(键名见 backend/.env,值不摘录) | 服务器 / 交付方 |
| F5 | test / staging 两套环境 | docs/cms/requirements.md:78-90 强制要求 dev/test/staging/prod 四环境且数据库、上传目录、缓存按环境隔离;交付只有一份 .env,w2r.site 与 demo.w2r.site 同机(47.242.46.253) | 「上 prod 前在 staging 跑验证清单」(requirements.md:107-115)这条流程无法执行 | 甲方 IT |
| F6 | 配置晋级基线包 config/*.json | docs/cms/config-promotion.md:13-21 列出 8 个基线文件(content_models.json、validation_rules.json、i18n_policies.json、templates.json、component_whitelist.json、roles.json、permissions.json、channel_auth.json),全树均不存在 | 接口 GET /api/config/export(Routes.php:97)能导出,但没有一份已知良好的基线可导入新环境 | 需从生产库现导 |
| F7 | 服务器 / 面板 / 数据库账号 | 属凭据类,交付树中不存在(正确做法) | 无账号则以上所有服务器侧交接无法执行 | 甲方 IT + 交付方 |
| F8 | 构建用 Makefile | demo.w2r.site/.gitignore:2 注释「Makefile 编译标记,勿提交」,对应标记文件 demo.w2r.site/.frontend-build-done 确实存在(0 字节);但全树 find -iname "Makefile*" 为空 | 前台构建流程的自动化入口缺失,只能手敲 npm run build | 交付方 |
G. 测试与 CI/CD:等同于没有
| # | 缺失项 | 证据 | 影响 | 谁持有 |
|---|---|---|---|---|
| G1 | 后端核心模块零测试 | w2r.site/backend/tests/ 全部内容:unit/Figma/ 下 5 个 Figma 测试 + unit/HealthTest.php + CI4 自带的 4 个 Example 样板(_support/、database/ExampleDatabaseTest.php、session/ExampleSessionTest.php)。phpunit.xml.dist:36-38 覆盖范围写的是整个 ./app | 认证、MFA、RBAC、工作流、发布、预览、DAM 分片、搜索、审计——一个测试都没有。27,252 行 PHP 里只有 Figma 导入被测 | 交付方 |
| G2 | 前端零测试 | w2r.site/admin-frontend/package.json scripts 只有 dev / build / preview;demo.w2r.site/frontend/package.json 同样只有三条。无 vitest / jest / playwright 依赖 | 两个 Vue 应用无任何自动化验证 | 交付方 |
| G3 | CI 无 build / test / deploy | w2r.site/.gitlab-ci.yml 全文只有 stages: [test, secret-detection] + 引入 Security/SAST.gitlab-ci.yml 与 Security/Secret-Detection.gitlab-ci.yml;backend/.gitlab-ci.yml、admin-frontend/.gitlab-ci.yml 内容相同(1,025 B);demo.w2r.site 连 .gitlab-ci.yml 都没有 | 没有构建流水线,没有制品追溯。backend/public/AdM/ 里的构建产物与 admin-frontend/js/ 源码之间无法证明对应关系(public/AdM/index.html 时间戳 2026-06-21 22:14,源码到 2026-07-22;.vite/manifest.json 列出的 13 个 view 与源码一致,故不能断定产物过期——推断:需重新构建比对) | 交付方 |
| G4 | 测试与覆盖率报告 | backend/build/logs/ 只有 testdox.txt / testdox.html / logfile.xml;phpunit.xml.dist:21 配置的 build/logs/coverage.serialized 不存在 | 无法证明任何质量水位 | 交付方 |
| G5 | 近期安全扫描结果 | w2r.site/security-scan-results/ 全部 7 个文件时间戳均为 20260125_011114,即 2026-01-25 单次扫描;而 Figma 导入、修订稿、预览改造等大量功能在 3–7 月才写 | 半年多的新代码从未扫过。security-scan.sh(52 KB)与 SECURITY-SCAN-README.md 也被 .gitignore:8,10 排除在仓库外 | 交付方 |
| G6 | 等保测评 / 渗透测试报告 | docs/project-introduction.md:25 与 docs/cms/requirements.md:162 均写「等保二级导向」。全树无任何测评报告、备案证明、渗透报告 | 「导向」≠「已过测评」。上线合规风险敞口:没有任何第三方证据证明达到等保二级 | 甲方合规部门 / 第三方测评机构 |
H. 官网前台 demo.w2r.site 的三个月代码代差(本次最重发现)
交付的 demo.w2r.site 源码时间停在 2026-03-23(backend/public/api.php、frontend/src/router/index.js、frontend/README.md 皆为该日)。而 CMS 的 docs/changelog.md 在 4–6 月记录了大量已在 demo 上验证通过的功能。这些功能在交付的 demo 代码里一律不存在。
| # | 缺失项 | 证据 | 影响 | 谁持有 | |
|---|---|---|---|---|---|
| H1 | api.php 缺 4 类线上端点 | 交付的 demo.w2r.site/backend/public/api.php(683 行)全部路由只有 5 条:/api/asset/{id}(:12)、/api/menu(:24)、/api/news/{uuid}(:31)、/api/news(:37)、/api/hello(:52)。而 w2r.site/docs/changelog.md:206 写「验证:GET https://demo.w2r.site/api/home?lang=zh 返回新 css_url 与正文」,:168 写 /api/page-css/ 已进 api.php,:172 写 /api/preview/ 已转发 | 首页动态内容接口、通用页面接口、预览接口、页面 CSS 交付接口全部不在交付内。用这份代码重建,线上首页会直接空白 | 47.242.46.253 上的 /www/wwwroot/demo.w2r.site/ | |
| H2 | 前端只有 2 条路由 | demo.w2r.site/frontend/src/router/index.js 全文只注册 /、/en/、/en、/news/:uuid、/en/news/:uuid;src/pages/ 只有 HomePage.vue、NewsDetailPage.vue。而 changelog :150-157 明确「demo 站新增 /preview/:token 路由」与 PreviewPage.vue | 预览页、通用内容页(page / faq_page / blog)、Chronology 子页、媒体中心全部不存在 | 同上 | |
| H3 | PreviewPage.vue、hydrateChronologyLayout.js | changelog :174(PreviewPage.vue 登录表单改用户名+密码)、:216(hydrateChronologyLayout.js 按 Figma 间距重排年份);find . -iname "PreviewPage" -o -iname "hydrateChronology" 全树为空 | 同 H2 | 同上 | |
| H4 | .htaccess 缺 page-css / preview 转发 | changelog :168-176 说 demo 站 .htaccess 已增加 page-css 转发与 RewriteRule ^preview(/.*)?$ api.php;交付的 demo.w2r.site/backend/public/.htaccess 只有 `RewriteRule ^(hello\ | )$ api.php,frontend/public/.htaccess 只有 /asset/{id} → /api/asset/{id}` 的 302 | 与 H1 互为佐证:交付的是 3 月快照 | 同上 |
| H5 | shared/dam-writable 是断链 | demo.w2r.site/shared/dam-writable -> /www/wwwroot/w2r.site/backend/writable,test -e 判定为 DANGLING SYMLINK(因 A2 缺失) | demo 站取不到任何 DAM 文件,图片、视频、文档全 404 | — | |
| H6 | 媒体账号体系(注册→审批→激活→MFA) | demo.w2r.site/.cursor/rules/media-permissions-mfa.mdc:7-11 描述「部分观看/下载需媒体权限(注册 → 后台审批 → 激活)」「媒体账号登录必须通过 MFA」。交付的 demo backend 只有 5 个 Controller(News.php、Home.php、BaseController.php、AssetDeliver.php、Api.php)和 1 个 Model(ChannelModel.php),无任何用户/权限/MFA 代码 | 规则描述的能力在交付代码中完全不存在 | 同上 | |
| H7 | 前台搜索页 | docs/cms/search-design.md 与 acceptance-checklist.md:35-41 要求站内搜索、联想、热词、置顶;demo src/pages/ 无搜索页,api.php 无搜索端点 | 验收项「搜索」在前台无法演示 | 同上 | |
| H8 | 页脚法务页面全是占位 | demo.w2r.site/frontend/src/components/home/SiteFooter.vue:44-48:FAQ / Help / Sitemap / Privacy Policy / Legal Statement 五项 href: '#';全树 href: '#' 共 6 处 | 隐私政策、法律声明页面未交付。与 SiteFooter.vue:51-52 已硬编码的备案号(京ICP备08103718-21、京公网安备 11010502040869号)并列,合规上是明显缺口 | 甲方法务 |
I. CMS 后台:文档承诺的能力在前端没有入口
后端接口大多存在(backend/app/Config/Routes.php 共 172 行、约 90 条路由),但管理端只交付了 13 个页面(admin-frontend/js/router/index.js:dashboard / channel / news / content / faq / media / asset-categories / audit / users / roles / settings + login + mfa-enroll)。
| # | 缺失项 | 证据 | 影响 | 谁持有 | ||
|---|---|---|---|---|---|---|
| I1 | 可视化拖拽编辑器未接入 | admin-frontend/js/cms/components/VisualEditor.vue 文件存在,但 grep -rn "VisualEditor" --exclude-dir=node_modules . 全树零命中 —— 没有任何文件 import 它。docs/project-introduction.md:30 把「固定模板与可视化拖拽相结合」列为核心目标 | 专题页/落地页的拖拽编辑能力实际不可用,是死代码 | 交付方 | ||
| I2 | 审核工作台页面 | 后端 Routes.php:117-120 有 /api/workflow/submit、review-l1、review-l2、actions;admin-frontend/js/views/ 无对应页面;dev-plan.md:118-119 要求「审核列表页面(待初审、待终审)」 | 2 级审核只能靠接口调,acceptance-checklist.md:8-12 的工作流验收项无法在 UI 上演示 | 交付方 | ||
| I3 | 搜索热词 / 置顶管理页面 | 后端 Routes.php:163-171 有完整 CRUD;前端无页面(grep -rln "hotword" js/ 仅命中 menu-permissions.js 之类的配置) | acceptance-checklist.md:37-38「热词可配置」「置顶可配置」无 UI | 交付方 | ||
| I4 | FAQ 主题管理页面 | 后端 Routes.php:107-111 有 faq/topics CRUD;前端只有 views/FaqQuestions.vue(题库),无主题页 | acceptance-checklist.md:51「可在后台管理 FAQ 主题(双语)」无 UI | 交付方 | ||
| I5 | 响应式断点配置与 ?device= 预览参数 | docs/cms/requirements.md:146-155 与 acceptance-checklist.md:73-76 要求编辑器设备切换、按断点配置组件、预览链接带 `?device=mobile\ | tablet\ | desktop;Routes.php` 无 device 参数处理,前端无设备切换器 | 4 条验收项无法通过 | 交付方 |
| I6 | i18next 未安装 | docs/cms/requirements.md:128 写管理端技术栈为「Vue 3 + Vite + Element Plus + TypeScript + i18next」;admin-frontend/package.json 依赖里没有 i18next(有 element-plus、pinia、vue-router、vuedraggable、qrcode、uuid、lodash-es) | 管理端界面无国际化框架,界面文案中文硬编码(如 router/index.js:102 的固定标题) | 交付方 |
J. 第三站点与外部资产
| # | 缺失项 | 证据 | 影响 | 谁持有 |
|---|---|---|---|---|
| J1 | 139.224.64.38 / vgc.digirepub.com 零代码交付 | docs/project-introduction.md:44 把它标为「生产域名」、:45 管理后台 https://vgc.digirepub.com/AdM/;交付的两个 zip 只来自 47.242.46.253 | 文档声称的生产环境完全没有交付物。这台机器上跑的是什么版本、和交付代码差多少,无从判断 | 甲方 IT / 交付方 |
| J2 | Figma 设计源文件与团队访问权 | backend/.env:61-64 配了 figma.token 等四项;docs/cms/figma-import.md、demo.w2r.site/frontend/docs/design-tokens-source.md 大量引用 Figma 节点号(157:144、9002:8010、172:159 等);demo.w2r.site/frontend/src/assets/figma/mcp-assets.js:3 自述「新闻详情帧(172:159)等仍为 Figma MCP 外链,过期前需替换或同步导出」 | Figma 一旦失去访问,页面无法重新导入,已知有外链资产会过期失效 | 设计方 / 甲方品牌部门 |
| J3 | 品牌字体授权文件 | demo.w2r.site/frontend/src/assets/fonts/TheGroupHEAD-Light.ttf、TheGroupTEXT-Regular.ttf 随包分发,并已打进构建产物(frontend/dist/assets/TheGroupTEXT-Regular-DE_XiI3J.ttf);全树无 LICENSE、无 EULA、无授权书 | Web 字体的分发授权无书面依据 | 甲方品牌部门 |
| J4 | 域名注册与续费凭据 | 已知 w2r.site 2026-02-08 经阿里云万网注册、2027-02-08 到期 | 到期无人续则整站不可达(含 demo.w2r.site 与 CMS) | 交付方(注册账号) |
| J5 | Sitecore 访问凭据的归属与有效期 | backend/.env:16-19 内含 Sitecore 登录地址与账号(值见该文件对应行,不在此摘录) | 凭据属于谁、能用到什么时候、旧站下线后是否失效,均未说明 | 现有官网运维方 |
附:本次排查中被证伪的几个直觉
demo.w2r.site/frontend/src/assets/video/home.mp4在(20 MB,2026-03-23),前端构建不会因缺视频而失败。此前基于ls -R截断的判断有误,已更正。- 数据库结构可重建:51 个迁移 +
RolePermissionSeeder.php(含 7 个角色定义,与project-introduction.md:179-187完全一致)+CreateSuperAdminCLI 命令,足以起一个空库。缺的是数据不是结构。 - CSP 确实开着:
backend/app/Config/App.php:201CSPEnabled = true,ContentSecurityPolicy.php的scriptSrc / styleSrc / fontSrc均为'self'。acceptance-checklist.md:66-67这两项在配置层面是达标的。 docs/的图片与图表齐全:docs/images/5 张流程图、docs/context-diagram.png、development-diagram.png、docs/cms/diagrams/2 个.mmd全部在位,scripts/gen_diagram_images.py也在。
服务器部署:生产环境与测试环境(38 项)
服务器部署:生产环境与测试环境
0. 一句话结论
交付物里不存在「生产」与「测试」两套环境的区分——只有一台机器(47.242.46.253)上并排的两个站点目录,两站的 CI_ENVIRONMENT 都是 development,共用同一台 MySQL、同一个库名 vgc、同一份 DAM 媒体目录(靠符号链接跨站点直连)。设计文档里写满了 dev / test / staging / prod 四环境与隔离要求,代码与配置里一条也没落地。 真正对外的生产域名 vgc.digirepub.com(139.224.64.38)没有任何代码、配置或凭据交付,其归属与上线状态在仓库中无从判断。
1. 设计上打算怎么分环境(文档侧)
| 来源 | 要求 | 落地情况 |
|---|---|---|
docs/project-introduction.md:37 | 「多环境(dev/test/staging/prod)与配置可导入导出,密钥不入库」 | 未落地 |
docs/project-introduction.md:253 | 「DB / 上传 / 缓存按环境隔离;密钥仅环境变量」 | 相反:全部共享 |
docs/cms/requirements.md:80-83 | 四环境定义,staging「尽量接近 prod(WAF/CDN/CSP/鉴权)」 | 无 staging |
docs/cms/requirements.md:87-89 | 强制隔离:数据库按环境隔离、上传目录按环境隔离、缓存按环境隔离 | 三条全部违反,见 §4 |
docs/cms/config-promotion.md:296-301 | 流转链 dev → test → staging → prod | 无任何环境流转机制 |
docs/cms/config-promotion.md:310-320 | 上 prod 前的 6 项 staging 验证清单 | 无 staging 可执行 |
docs/project-introduction.md:279 | 规则 070-cms-multi-env.mdc 职责为「多环境隔离、配置晋级、staging 验证清单」 | 规则文件本身仓库中无此项(实交付 2 条 rules,见 demo.w2r.site/.cursor/rules/,只有 frontend-build.mdc 与 media-permissions-mfa.mdc) |
注意docs/project-introduction.md:25与docs/cms/requirements.md:162的措辞是「等保二级导向」——是设计参照,不是已通过测评的结论。多环境隔离恰恰是等保测评会查的项,现状不满足。
2. 环境判定:交付的 .env 是 development,不是 production
这是本议题最硬的一条事实。
| 文件 | 行 | 值 |
|---|---|---|
w2r.site/backend/.env | 5 | CI_ENVIRONMENT = development |
demo.w2r.site/backend/.env | 5 | CI_ENVIRONMENT = development |
w2r.site/backend/env(CI4 官方模板,未使用) | 17 | # CI_ENVIRONMENT = production(注释态) |
ENVIRONMENT 常量在代码里分叉 11 处,development 与 production 的行为差异:
| 位置 | development 行为 | production 行为 |
|---|---|---|
app/Config/Boot/development.php:13-14 | error_reporting(E_ALL)、display_errors = 1 | app/Config/Boot/production.php:12-15:display_errors = 0 |
app/Config/Boot/development.php:24 | SHOW_DEBUG_BACKTRACE = true(异常页输出完整栈与文件绝对路径) | production 不定义此常量 |
app/Config/Boot/development.php:34 | CI_DEBUG = true | production.php:25:CI_DEBUG = false |
w2r.site/backend/app/Config/Events.php:41 | 注册 __hot-reload 路由(生产站点上多一个可被探测的端点) | 不注册 |
demo.w2r.site/backend/app/Config/Events.php:45-53 | 无环境判断,只看 CI_DEBUG 就挂 Debug Toolbar 并 service('toolbar')->respond() | 同上(靠 CI_DEBUG 关) |
app/Config/Logger.php:42 | threshold = 9(全量) | threshold = 4 |
app/Config/Security.php:85 | $redirect = false(CSRF 失败返回而非重定向) | $redirect = true |
app/Config/Jwt.php:70-72 | JWT 密钥为空时随机生成,进程重启即全体登出 | app/Services/JwtService.php:31:抛 RuntimeException |
app/Views/errors/html/error_404.php:76、error_400.php:76、error_exception.php:30 | 错误页回显 ENVIRONMENT 与内部信息 | 不回显 |
这不是纸面推断。 demo.w2r.site/backend/writable/debugbar/ 里有 425 个 debugbar_*.json,最新一个 debugbar_1774268703.139049.json 对应 2026 年 3 月下旬——说明这台对外可访问的机器确实以 debug 模式接过真实请求。同目录 writable/logs/log-2026-02-28.log:1 起的堆栈把 /www/wwwroot/demo.w2r.site/backend/vendor/... 绝对路径写进了日志,同样的信息在 display_errors=1 下会直接打到浏览器。
w2r.site/backend/.env:37 的 JWT_SECRET 值本身带着 do-not-use-in-production 字样(值见 w2r.site/backend/.env:37),.env:18 还明文躺着 Sitecore 生产 CM 的管理员口令(见 w2r.site/backend/.env:17-18),.env:61 有 Figma PAT(见该行)。这三条凭据在交付包里是明文,必须按已泄露处理并轮换。
3. 从 .htaccess / .user.ini 还原出的目录结构与改写规则
Apache 语法的改写规则一共 6 份,还原出的假设结构:
3.1 w2r.site(CMS)
DocumentRoot = /www/wwwroot/w2r.site ← 站点根就是项目根
.htaccess:13 ^(.*)$ → backend/public/$1 (非真实文件才改写,:9-11)
.htaccess:6-7 已在 /backend/public/ 下的请求不再二次改写
.user.ini:1 open_basedir=/www/wwwroot/w2r.site/:/tmp/
backend/public/.htaccess:7 RewriteBase /
backend/public/.htaccess:14-16 非文件非目录 → index.php/$1
backend/public/.htaccess:18-19 透传 Authorization 头(JWT 依赖)
把 CI4 项目根直接当站点根、靠 .htaccess 二次改写进 backend/public,是 CI4 官方明确反对的做法(w2r.site/backend/README.md:22-24 原文就在批评这一点)。后果:.htaccess 一旦失效(换 nginx、AllowOverride 非 All),app/、.env、vendor/、writable/ 全部裸露在 HTTP 根下。
3.2 demo.w2r.site(官网前台)
DocumentRoot = /www/wwwroot/demo.w2r.site/frontend/dist ← 推断,见下
demo.w2r.site/.htaccess:1 内容只有一个空格(等于空)→ 站点根不是 doc root
demo.w2r.site/index.html:31-35 宝塔默认页(「面板系统后台 > FTP」),未被覆盖
frontend/dist/.htaccess:19 SPA fallback → /index.html
frontend/dist/.htaccess:16 /asset/{id} → /api/asset/{id} 302
frontend/dist/.htaccess:2-9 mp4/webm/字体 Cache-Control immutable 1 年
backend/public/.htaccess:15 RewriteBase /api/ ← CI4 挂在 /api 子路径
backend/public/.htaccess:29 ^(hello|)$ → api.php
backend/public/.user.ini:1 opcache.enable=0
.user.ini:1 open_basedir=/www/wwwroot/demo.w2r.site/:/tmp/
shared/dam-writable 符号链接 → /www/wwwroot/w2r.site/backend/writable
backend/public/.htaccess:23-25 有一条 www. → 非 www 的 301,且目标写死 http://(非 HTTPS)。站点已签发证书(见 §4),这条规则会把 www 访问降级到明文 http 再跳。
3.3 open_basedir 与符号链接冲突(需服务器核实)
demo.w2r.site/.user.ini:1 把 demo 的 PHP 文件访问限制在 /www/wwwroot/demo.w2r.site/ 与 /tmp/。但 demo.w2r.site/backend/public/demo_asset_delivery.inc.php:68-69 用 realpath() 解析 shared/dam-writable,真实路径是 /www/wwwroot/w2r.site/backend/writable,落在 open_basedir 之外。open_basedir 按解析后的真实路径判定,理论上所有 DAM 图片交付都应被拒。推断:线上要么在宝塔站点配置或 php-fpm pool 里额外放行了 w2r.site 目录,要么这份 .user.ini 与线上不一致。这是必须在服务器上实测的一条。
4. 生产环境形态的证据(全部为直接证据)
| 证据 | 位置 | 说明 |
|---|---|---|
| nginx 默认 404 页 | w2r.site/backend/public/404.html:6、demo.w2r.site/404.html:5 | 页脚 <center>nginx</center> |
| 宝塔面板默认站点页 | w2r.site/backend/public/index.html:31-35、demo.w2r.site/index.html:31-35 | 「本页面在 FTP 根目录下的 index.html」「面板系统后台 > FTP」 |
| Let's Encrypt ACME 挑战文件 | demo.w2r.site/.well-known/acme-challenge/nk2ypvpch9dpho1xjfyozelzg70mzqma | HTTP-01 验证残留,证书由面板签发 |
| 明写 nginx 的部署要求 | demo.w2r.site/frontend/README.md:24 | 「Nginx:将 location ^~ /api/ 转发到能执行 api.php 的配置(与现有 /api/menu 一致)」 |
| 静态映射依赖 web server | w2r.site/backend/app/Services/Figma/EntryCssVersionService.php:12 | 「/page-css/{filename}(由 nginx/Apache 静态映射 WRITEPATH/uploads/page-css)」 |
| 运维边界描述 | docs/cms/audit-gap-implementation.md:246 | 「nginx、php-fpm、MySQL 由运维」 |
| open_basedir 两站独立 | w2r.site/.user.ini:1、demo.w2r.site/.user.ini:1 | 说明是 php-fpm / FastCGI,且按站点分池(.user.ini 只在 FPM 下生效) |
| 早期站点目录名 | w2r.site/SECURITY-SCAN-README.md:41 | cd /www/wwwroot/vgc —— 与库名 vgc(w2r.site/backend/.env:26)同源,说明项目搬过目录 |
| 跨站点符号链接 | demo.w2r.site/shared/README.md:8-9 | ln -sfn /www/wwwroot/w2r.site/backend/writable /www/wwwroot/demo.w2r.site/shared/dam-writable |
| PHP 环境限制被绕过 | demo.w2r.site/backend/README.md:7 | 「服务器 PHP 环境限制(putenv 禁用、mbstring 扩展未安装),当前使用轻量级 API 入口 public/api.php 绕过完整 CI4 引导」 |
| 探测脚本留在 web 根 | w2r.site/backend/public/test-putenv.php | 对外可访问,回显 disable_functions 与 DotEnv 源码行;同目录还有 groupui-showcase.html |
| writable 权限 0777 | demo.w2r.site/backend/writable/ 及其全部子目录(ls -la 显示 drwxrwxrwx) | 全局可写 |
| CORS 全开 | demo.w2r.site/backend/public/api.php:19 | Access-Control-Allow-Origin: * |
| 无任何站点配置交付 | find 全树只在 localdev/(接手方自建)下找到 .conf 文件 | 仓库中无 nginx / apache 站点配置、无 php-fpm pool 配置、无 crontab、无宝塔备份 |
4.1 一个必须解释的矛盾
demo.w2r.site/backend/README.md:7 说这台机器的 PHP 没有 mbstring、禁用了 putenv;而 w2r.site/backend/composer.json:13-17 要求 php ^8.2 + ext-intl + ext-mbstring + ext-mysqli + ext-imagick,w2r.site/backend/.env 又必须靠 CI4 DotEnv(依赖 putenv 或 $_ENV 写入)加载。推断:宝塔按站点绑定不同 PHP 版本/池,w2r.site 用的是装齐扩展的那一个,demo.w2r.site 用的是另一个。demo 的 backend/public/index.php:6-9 也确实额外加载了 symfony/polyfill-mbstring 兜底。这条必须向交付方确认——它决定新环境要装几套 PHP。
5. 配置晋级机制(ConfigPromotionService 与 /api/config/export|import)
5.1 实际做了什么
- 路由:
w2r.site/backend/app/Config/Routes.php:97-98,只有GET config/export与POST config/import,均挂authfilter;权限点在控制器里判,ConfigPromotionController.php:26(config:export)与:68(config:import)。 - 支持类型:
ConfigPromotionService.php:13→workflow、seo、rbac、channel_auth、channels。 - 导出实现:
:38-45分发;:110-131rbac 直连roles/permissions/role_permissions三表;:133-144channel_auth 连channel_role_permissions;:146-149channels 全表findAll()。 - 导入实现:
:85-92,其中 rbac 直接拒绝(:88「请使用专用脚本或分步导入 channel_auth」),channels 直接拒绝(:90「暂未开放」)。真正能导入的只有workflow与seo两类,外加channel_auth。
5.2 与文档不符之处
| 文档承诺 | 实际 | |||
|---|---|---|---|---|
config-promotion.md:209 类型为 `content_models\ | workflows\ | rbac\ | search` | 五个类型名全部对不上(ConfigPromotionService.php:13) |
config-promotion.md:176-179 主版本号不同拒绝导入 | 无任何版本校验:version 只在 :47-56、:98-107 原样回显 | |||
config-promotion.md:256 dry_run 为「仅验证」 | :72-83 直接返回 validated: true,不做任何校验就说通过 | |||
config-promotion.md:358-360 导入前自动备份到 writable/backups/config/ | 代码中无备份逻辑;writable/ 整个未交付 | |||
config-promotion.md:364 POST /api/config/restore | 仓库中无此路由(Routes.php 无 restore) | |||
config-promotion.md:286-290 export-all / import-all | 仓库中无此项 | |||
config-promotion.md:13-27 表里的 content_models.json、templates.json、component_whitelist.json、roles.json、permissions.json、i18n_policies.json 等 8 个配置文件 | 仓库中无 config/ 目录、无这些 json 文件 | |||
config-promotion.md:340-343 密钥变量名 DB_PASSWORD / PREVIEW_PASSWORD / MFA_SECRET / ENCRYPTION_KEY | .env 实际键名是 database.default.password(.env:28);后三个仓库中无此项,app/Config/Encryption.php:24 的 $key 是空串 |
5.3 用它搬配置的风险
- 覆盖式写入,无回滚。
ConfigPromotionService.php:209的replaceForChannel()是整栏目授权替换;一次错误的channel_auth导入会把该栏目的全部角色权限规则清空重建,且没有备份可回退(见上表)。 dry_run给假的安全感。 演练返回「校验通过」不代表真实导入会通过,:72-83根本没跑到importXxx。- 没有环境归属标记。 导出体(
:47-56)里只有type/version/exported_at,没有源环境标识。目前两站共库(§6),从哪导的、往哪导的,事后只能靠审计日志的actor推断(审计写在ConfigPromotionController.php:40-57与:109-122)。 - 它搬不动真正难搬的东西。 内容模型、模板、组件白名单、搜索热词/置顶都不在支持类型里;数据库结构靠 51 个迁移(
w2r.site/backend/app/Database/Migrations/,ls | wc -l= 51)+ 1 个 seeder(Seeds/RolePermissionSeeder.php)。全库无.sql导出(find . -name "*.sql"无结果),初始数据只能靠迁移与 seeder 重建。
6. 「测试环境」判断:现在没有,一台机器上是两个站点而不是两套环境
6.1 证据
| 维度 | w2r.site(CMS) | demo.w2r.site(官网前台) | 结论 |
|---|---|---|---|
| 服务器 | 47.242.46.253 | 47.242.46.253 | 同机 |
CI_ENVIRONMENT | development(.env:5) | development(.env:5) | 都不是 production |
| DB 主机 | localhost(.env:25) | localhost(.env:11) | 同一实例 |
| DB 库名 | vgc(.env:26) | vgc(.env:12) | 同一个库 |
| DB 账号 | vgc(.env:27) | vgc(.env:13) | 同一账号 |
| 媒体存储 | WRITEPATH(app/Config/Paths.php:56) | 符号链接到 w2r 的同一目录(shared/README.md:9、.env:24) | 同一份文件 |
| 表共用 | — | backend/app/Models/ChannelModel.php:8「与 w2r.site 栏目管理共用 channels 表」;app/Controllers/News.php:9「共用 entries / entry_i18n」 | 直连同表 |
docs/cms/requirements.md:87-89 要求数据库、上传目录、缓存三项按环境隔离——现状是三项全部共享。demo.w2r.site 不是 w2r.site 的测试环境,它是同一套系统的官网前台。 在 CMS 后台改一条内容,官网立刻变;在官网上做压测,改的是 CMS 的同一批数据。
6.2 其他佐证
- 无 CI/CD 部署管线。
w2r.site/.gitlab-ci.yml全文只 include 了Security/SAST.gitlab-ci.yml与Security/Secret-Detection.gitlab-ci.yml(第 12-13、21-22 行),stages 只有test与secret-detection,没有 build、没有 deploy、没有 environment 定义。部署只能是手工。 - 仓库边界不完整。
w2r.site/backend/.git/config:10→gitlab.onedevops.vw.com.cn/c-sdxx/corporate-website/backend.git;w2r.site/admin-frontend/.git/config:10→.../frontend.git;w2r.site/README.md:18→.../corporate-website.git。demo.w2r.site整个目录没有.git(find demo.w2r.site -name .git -type d无结果)——官网前台不在版本控制里。 .gitignore把环境相关物全排除了。w2r.site/.gitignore:5,7,11,12,13,14,15排除docs/、scripts/、writable/、backend/.env、backend/writable、.htaccess、.user.ini。这解释了为什么服务器配置从来没进过仓库,也意味着没有任何一份「环境配置」是可版本化、可 diff、可回滚的。- 交付的构建产物比源码旧。
w2r.site/backend/public/AdM/index.html的 mtime 是 2026-06-21 22:14,而w2r.site/admin-frontend/js/下最新源文件是 2026-07-22 09:30——后台 SPA 打包产物落后源码约一个月。demo.w2r.site/frontend/dist/是 2026-06-12 15:55,src/components/home/HeaderNav.vue与src/lib/nav/megaMenuState.js是同日 16:00,落后 5 分钟。(这是 zip 内保留的 mtime,为推断依据;上线前必须以重新构建为准,构建配置见w2r.site/admin-frontend/vite.config.js:8(base: '/AdM/')、:11(outDir: ../backend/public/AdM)与package.json:8(build 后删除产物里的index.php、.htaccess)。) - 域名本身就是临时的。
w2r.site于 2026-02-08 经阿里云万网注册、2027-02-08 到期;生产域名却是vgc.digirepub.com(docs/project-introduction.md:44)。而w2r.site/backend/app/Config/App.php:19把baseURL硬编码成https://w2r.site/,w2r.site/admin-frontend/js/utils/contentPreview.js:2把PUBLIC_SITE_ORIGIN硬编码成https://demo.w2r.site(会被打进 AdM 包),app/Services/SeoSchemaService.php:20默认base_url是https://w2r.site,app/Commands/AuditAcceptance.php:26硬编码httpHost = 'w2r.site'。换域名不是改配置,是改源码再重新构建。 后台设置页的占位符倒是写着https://vgc.digirepub.com(admin-frontend/js/views/Settings.vue:206),说明当初预期过要换。
6.3 接手方已经证明「能重建」
localdev/ 是接手流程自建的本机还原(localdev/.env:1 注明「由接手流程自动生成」),不属于交付物,但它是一份可用的环境规格:localdev/Dockerfile:3 用 php:8.2-apache,:17-19 装 intl mbstring mysqli pdo_mysql zip gd exif + imagick,:20 开 rewrite headers,:24-29 放宽 upload_max_filesize / post_max_size / memory_limit / max_execution_time;localdev/vhosts.conf:8 与 :27 分别把 w2r 站点根设为项目根、把 demo 站点根设为 frontend/dist 并 Alias /api 到 backend/public(:35)。这套配置能把两站跑起来,可以直接作为测试环境的起点。
7. 生产与测试环境应有的差异表
| 配置项 | 位置 | 测试 / staging | 生产 | 现状 |
|---|---|---|---|---|
CI_ENVIRONMENT | backend/.env:5 | testing 或 development | production | 两站都是 development |
display_errors / CI_DEBUG | app/Config/Boot/*.php | 可开 | 必须关 | 开着,有 debugbar 实证 |
| Debug Toolbar | demo/app/Config/Events.php:45 | 可开 | 必须关(当前只受 CI_DEBUG 控制) | 开着 |
__hot-reload 路由 | w2r/app/Config/Events.php:41 | 可开 | 不注册 | 生产会注册 |
app.baseURL | app/Config/App.php:19(硬编码) | 测试域名 | 生产域名 | 硬编码,须改为读 .env 的 app.baseURL |
PUBLIC_SITE_ORIGIN | admin-frontend/js/utils/contentPreview.js:2 | 测试前台域名 | 生产前台域名 | 硬编码进构建产物 |
forceGlobalSecureRequests | app/Config/App.php:160 | 可 false | true | 两站均 false |
Cookie secure | app/Config/Cookie.php:57 | 可 false | true | false |
CSPEnabled | w2r App.php:201 = true / demo App.php:201 = false | 一致 | 一致且开启 | 官网前台无 CSP |
JWT_SECRET | backend/.env:37 | 独立随机值 | 独立随机值,且与测试不同 | 单一值,字面写着不可用于生产 |
encryption.key | app/Config/Encryption.php:24 | 独立 | 独立 | 空串,.env 中无此项 |
| 数据库 | .env:25-31 | 独立实例或独立库名 | 独立实例、独立账号、最小权限 | 两站同实例同库同账号 |
| DAM 存储根 | app/Config/Dam.php:49-50、demo .env:24 | 独立目录 | 独立目录 | 跨站符号链接共用一份 |
writable/ 权限 | — | 0755,属主 = php-fpm 用户 | 0750 或 0755 | demo 现为 0777 |
open_basedir | .user.ini:1 | 含站点根 + tmp + DAM 根 | 同左,最小化 | demo 与其符号链接目标冲突(§3.3) |
robots.txt | backend/public/robots.txt、demo/backend/public/robots.txt | Disallow: / | 正常放行 | 两份都是全站放行,测试站会被搜索引擎收录并与正式站争排名 |
| CORS | demo/backend/public/api.php:19 | 可宽松 | 白名单 | * |
opcache.enable | demo/backend/public/.user.ini:1 = 0 | 可关(便于改代码即生效) | 开(性能) | 关着 |
| 调试文件 | backend/public/test-putenv.php、groupui-showcase.html | 可留 | 必须删 | 都在 web 根 |
| cron | 见 §8 | 可只跑 publish | 三条全开 | 无交付 |
| 审计日志保留 | CleanAuditLogs.php:25 默认 6 个月 | 可缩短 | 保留 ≥ 6 个月(等保口径) | 无 cron 则永不清理 |
8. crontab:应有条目
三个命令需要调度,都在 w2r.site/backend/app/Commands/ 下:
| 命令 | 定义位置 | 文档要求频率 | 建议 crontab |
|---|---|---|---|
publish:run-schedule | RunPublishSchedule.php:13($name),:14 注明「建议由 cron 调度」 | 每分钟(docs/cms/state-machine.md:292 定时发布、:298 定时下线) | * cd /www/wwwroot/w2r.site/backend && php spark publish:run-schedule >> /dev/null 2>&1 |
audit:cleanup [retention_months] | CleanAuditLogs.php:21($usage),默认 6 个月(:25) | 每月 1 日 0 点(CleanAuditLogs.php:14、docs/cms/audit-logging.md:290) | 0 0 1 cd /www/wwwroot/w2r.site/backend && php spark audit:cleanup |
logs:cleanup [retention_days] | CleanApplicationLogs.php:22($usage),默认 180 天(:27-28) | 每月 1 日,与 audit 同批(CleanApplicationLogs.php:15 给了完整示例行、docs/cms/logging-system-design.md:196) | 0 0 1 cd /www/wwwroot/w2r.site/backend && php spark logs:cleanup |
注意事项:
cd到backend/是必需的,CleanApplicationLogs.php:15的示例就是这么写的——CI4 的spark依赖工作目录定位app/与.env。- cron 用的 PHP 二进制可能不是
php。 宝塔多 PHP 版本共存时需写全路径(如/www/server/php/82/bin/php)。这条要在服务器上确认。 publish:run-schedule每分钟跑,且它自己写审计(RunPublishSchedule.php:22-40,operation = publish_schedule_run,actor_id = 0、actor_role = system)。每分钟一条 = 每月约 43,200 条审计,而audit:cleanup只按 6 个月保留期清理。上线前应确认PublishService::runSchedule()是否在无任务时也写审计——若是,operation_logs表会被系统任务淹没,检索性能与等保「可检索」要求都受影响。- 仓库中无 crontab 文件、无 systemd timer、无宝塔计划任务导出。 这三条目前是否在线上跑着,无从判断——需要向交付方索取
crontab -l与宝塔计划任务列表。定时发布是核心功能,若这条 cron 从未配置,那么所有定时发布/下线从上线起就是失效的。
9. 现状部署拓扑(含已知与未知)
[已知] 47.242.46.253 阿里云 · 宝塔面板 · nginx + php-fpm(多版本?) + MySQL(localhost)
├─ w2r.site CMS,CI_ENVIRONMENT=development
│ 站点根 = 项目根,.htaccess 改写进 backend/public
│ /AdM/ → 管理后台 SPA(backend/public/AdM,产物落后源码约 1 个月)
│ /api/* → CI4 Controllers
│ writable/ ← DAM 与运行时目录,【整个未交付】
├─ demo.w2r.site 官网前台,CI_ENVIRONMENT=development
│ 站点根 = frontend/dist(推断),/api → backend/public
│ /api/* 有两套并行实现:api.php(裸 PHP)与 CI4 Controllers,
│ 谁生效取决于【未交付的 nginx 配置】
│ shared/dam-writable ──符号链接──> w2r.site/backend/writable
└─ MySQL: 库 vgc,两站同库同账号(w2r .env:26 / demo .env:12)
[未知] 139.224.64.38 vgc.digirepub.com —— docs/project-introduction.md:44 标为「生产域名」
【无代码、无配置、无凭据、无访问方式交付】
与 47.242.46.253 是什么关系(同步?替代?废弃?)无从判断
[存量] 211.151.111.91 volkswagengroupchina.com.cn —— 现有 Sitecore 官网,被替代对象
w2r.site/backend/.env:16-18 存着它的 CM 后台地址与管理员凭据
官网页脚备案号已硬编码:京ICP备08103718-21、京公网安备 11010502040869号
(demo.w2r.site/frontend/src/components/home/SiteFooter.vue:51-52)
未知清单(仓库中无此项):nginx 站点配置、php-fpm pool 配置、PHP 版本与 disable_functions 实际值、crontab、SSL 证书与续期方式、数据库全量导出、w2r.site/backend/writable/ 全部内容、备份策略、WAF/CDN、监控告警、vgc.digirepub.com 的一切。
10. 上线所需的服务器清单
| # | 项 | 依据 | 说明 |
|---|---|---|---|
| 1 | Linux + nginx(或 Apache + mod_rewrite/mod_headers) | 404.html:6、frontend/README.md:24;localdev/Dockerfile:20 | 若继续用 nginx,所有 .htaccess 规则必须重写为 nginx location 规则——nginx 不读 .htaccess,交付包里那 6 份改写规则在生产上一条都没生效 |
| 2 | PHP 8.2+,扩展 intl mbstring mysqli imagick,另需 gd exif zip | backend/composer.json:13-17;app/Config/Images.php:14(默认 handler gd)、:20(libraryPath = /usr/local/bin/convert) | putenv 不得在 disable_functions 里(demo/backend/README.md:36-38) |
| 3 | PHP 上传/执行限制放宽 | app/Config/Dam.php:12(单文件上限 5 GB)、:17-18(分片 1–10 MB)、:23(最多 1,000 片) | upload_max_filesize / post_max_size ≥ 分片上限;max_execution_time 需覆盖合并耗时(localdev/Dockerfile:24-28 用的是 512 MB / 600 s) |
| 4 | MySQL 8,每环境独立库与账号 | .env:25-31;requirements.md:87 | 字符集 utf8mb4;搜索用 FULLTEXT(requirements.md:33) |
| 5 | ImageMagick 二进制 | app/Config/Images.php:20 指向 /usr/local/bin/convert | 路径需与实际安装位置一致,否则图片处理静默失败 |
| 6 | ffmpeg(若启用视频转码) | docs/cms/video-delivery-and-transcoding.md:54「依赖 ffmpeg(或同类工具)」 | app/Commands/ 下无转码 Worker 命令,只有 DeleteVideoMp4Assets.php;需确认视频转码一期是否真的上线 |
| 7 | Node + npm(构建期) | admin-frontend/package.json、demo/frontend/package.json | 两个前端都需重新构建,见 §6.2 |
| 8 | cron | §8 三条 | 含全路径 PHP 二进制 |
| 9 | 证书与续期 | .well-known/acme-challenge/ 残留 | 生产域名换成 vgc.digirepub.com 后需重新签发 |
| 10 | 磁盘容量 | DAM 单文件上限 5 GB(app/Config/Dam.php:12) | writable/ 未交付,现有媒体体量未知——这是容量规划的最大盲点 |
11. 上线所需的配置项清单
必须逐项确认,不能沿用交付包里的值:
| 配置项 | 位置 | 动作 |
|---|---|---|
CI_ENVIRONMENT | backend/.env:5 | 改 production |
app.baseURL | app/Config/App.php:19 硬编码 | 改造成读 .env(模板 backend/env:23 已预留 app.baseURL,只是被注释) |
PUBLIC_SITE_ORIGIN | admin-frontend/js/utils/contentPreview.js:2 | 改造成构建期变量,否则每换域名都要改源码 |
SEO base_url | app/Services/SeoSchemaService.php:20(默认值)+ 后台设置页(Settings.vue:206) | 上线后在后台设置为生产域名 |
| 数据库四件套 | backend/.env:25-31、demo/backend/.env:11-17 | 新库、新账号、新口令,两站不同库 |
JWT_SECRET | backend/.env:37 | 必须换(现值明文在交付包里,且字面标注 dev) |
encryption.key | app/Config/Encryption.php:24 为空、.env 中无 | 生成并写入 .env |
| Sitecore 凭据 | backend/.env:16-18 | 确认是否仍需要;若不需要则删除,若需要则轮换(现已随包泄露) |
| Figma PAT | backend/.env:61 | 同上,轮换或删除 |
asset.storageRoot | demo/backend/.env:24 | 指向生产 DAM 根,并同步调整 demo/.user.ini:1 的 open_basedir |
forceGlobalSecureRequests | app/Config/App.php:160 | 改 true(或由反代强制 HTTPS 并设 HSTS) |
Cookie secure | app/Config/Cookie.php:57 | 改 true |
CSPEnabled(官网) | demo/app/Config/App.php:201 | 与 requirements.md 的 CSP 要求对齐 |
| CORS | demo/backend/public/api.php:19 | 从 * 收敛为白名单 |
robots.txt | 两站 backend/public/robots.txt | 测试站改 Disallow: / |
open_basedir | 两站 .user.ini:1 | 按新路径重写,并解决 §3.3 的符号链接冲突 |
opcache.enable | demo/backend/public/.user.ini:1 | 生产开启 |
| 删除文件 | backend/public/test-putenv.php、backend/public/groupui-showcase.html | 上线前删 |
writable/ 目录树 | app/Config/Paths.php:56 起 | 需手工创建:logs/、cache/(Cache.php:84)、session/(Session.php:64)、uploads/dam/(Dam.php:50)、uploads/dam_chunks/(Dam.php:49)、uploads/page-css/(Figma.php:58)、fonts/(CaptchaService.php:162 需 arial.ttf);权限 0755,属主为 php-fpm 用户 |
/page-css/ 静态映射 | EntryCssVersionService.php:12、Figma.php:63(cssPublicPrefix = /page-css) | 已有 CI4 路由兜底(Routes.php:16),但文档建议由 web server 直出——两条路径二选一并写进站点配置 |
12. 搭一套独立测试/预发环境需要什么(最小可行)
- 一台独立主机或独立容器,不与生产共享 MySQL 实例与文件系统。
localdev/docker-compose.yml+localdev/Dockerfile+localdev/vhosts.conf已经是一份跑得通的规格,可直接改造为 staging。 - 独立数据库:跑 51 个迁移(
app/Database/Migrations/)+RolePermissionSeeder,得到空壳;业务数据必须向交付方索取全库导出,否则无法验证任何真实行为。 - 独立 DAM 目录:不能复用生产的
writable/,也不能用符号链接指回生产——这正是当前两站踩的坑。 .env三份分离:dev / staging / prod 各一份,密钥各不相同,且都不进仓库(.gitignore:12已排除)。- 把域名与前台 origin 从源码里挖出来(见 §11 前三行),否则 staging 会指向生产前台。
- 站点配置纳入版本控制:nginx conf、php-fpm pool、crontab、
.user.ini都要有一份可 diff 的副本。当前.gitignore:14-15把.htaccess与.user.ini排除在外,这个决定要推翻。 - 测试站
robots.txt设Disallow: /,并加 HTTP Basic 或 IP 白名单——现在demo.w2r.site是公网可达且全站放行收录的。 - 按
config-promotion.md:310-320的 6 项清单做 staging 验收(审核流程、预览链接、定时发布、缓存刷新、搜索生效、审计可查)。但要先补上 §5.2 列的那些「文档有、代码无」的缺口,否则清单里的「配置晋级」一项无法真正演练。
等保合规(网络安全等级保护)现状、差距与落地路径(27 项)
0. 结论先行
这个项目在等保上的真实状态是:代码里做了一部分二级技术项,但等保这件事本身一步都没走。 仓库里没有定级报告、没有备案证明、没有测评报告、没有专家评审意见、没有任何一份安全管理制度文件。文档里那句「等保二级导向」(docs/project-introduction.md:25、docs/cms/requirements.md:162)是设计时的自我参照,不是合规状态描述。
更要紧的是:即使按二级去测,当前代码状态也过不了。已实测确认的越权缺口(内容与栏目写接口无权限码校验)和一条我在核查中新发现的问题(backend/.env:37 的 JWT 签名密钥是人类可读的开发占位串)都会直接命中「高风险项」,而 GB/T 28448-2019 的判定规则是:只要存在导致高等级安全风险的问题,整体结论就是「不符合」,分数再高也没用。
1. 现状定级:「导向」「已备案」「已测评」是三件事
1.1 仓库里等保二字出现在哪
全仓(排除 vendor/、node_modules/)检索「等保 / 等级保护」,只有 7 处:
| 位置 | 原文要点 |
|---|---|
docs/project-introduction.md:25 | 「系统设计遵循等保二级导向,满足企业级安全与合规要求」 |
docs/cms/requirements.md:162 | 「基于安全要求(等保二级导向)实施 CSP 合规、MFA、审计、二次确认」 |
docs/cms/audit-gap-implementation.md:12 | 「对照等保/企业审计常见检查项,将当前未实现或部分实现的日志能力整理为可落地的开发清单」 |
docs/cms/logging-system-design.md:16、47 | 审计日志「满足等保与合规要求」 |
docs/cms/logging-system-design.md:185、188 | 审计/操作日志与安全事件「不少于 6 个月(180 天)」,依据栏写「等保」 |
backend/app/Commands/CleanAuditLogs.php:13 | 「满足等保『留存不少于 6 个月』后的自动清理要求」 |
仓库中无此项:定级报告、专家评审意见、《信息系统安全等级保护备案证明》、等级测评报告、差距分析报告、任何一份安全管理制度或应急预案。
1.2 三个状态的区别(这一段请原样转给甲方)
- 「等保二级导向」= 开发时拿二级的技术条款当参照物写代码。 没有第三方确认,没有行政备案,法律上不产生任何合规效力。当前项目就在这一档。
- 「已定级备案」= 行政动作。 运营使用单位自主定级、编制定级报告,报属地公安机关网安部门备案,拿到《备案证明》。依据《信息安全等级保护管理办法》(公通字〔2007〕43 号)第十五条,二级及以上系统应在安全保护等级确定后 30 日内到设区的市级以上公安机关备案。备案证明是一张纸,不代表系统安全。
- 「已测评」= 技术动作。 由公安部认可资质的第三方测评机构按 GB/T 28448-2019 现场测评,出具《等级测评报告》,再报公安备案。这才是「通过等保」的通俗含义。
三者是串行的:没定级就不能备案,没备案通常测评机构不接单,没测评报告就没有「通过」可言。
1.3 实际应该定几级
先说法规硬性要求。 新修订的《网络安全法》已于 2026 年 1 月 1 日施行(2025 年 10 月 28 日第十四届全国人大常委会第十八次会议修正)。等级保护义务条款由原第二十一条调整为第二十三条,其中明确「采取监测、记录网络运行状态、网络安全事件的技术措施,并按照规定留存相关的网络日志不少于六个月」;对应罚则由原第五十九条调整为第六十一条,罚款上限由 50 万元提至 100 万元,严重情形 50 万—200 万元。交接文档里凡引用「网络安全法第二十一条 / 第五十九条」的表述都已过时,必须改。 另外,《网络安全等级保护条例》至今仍是 2018 年征求意见稿,已列入国务院 2025 年立法预备项目,尚未正式发布——所以现阶段行政依据仍是 43 号文加各省公安实施口径。
再说定级本身。 按 GB/T 22240-2020《定级指南》,定级看两个要素:受侵害的客体(国家安全 / 社会秩序、公共利益 / 公民法人合法权益)与侵害程度。我从代码里能查到的事实:
| 判断维度 | 代码事实 | 对定级的影响 |
|---|---|---|
| 是否持有公民个人信息 | users 表只有内部员工账号(username / email / password_hash,见 backend/app/Database/Migrations/*CreateUsersTable.php);前台 demo.w2r.site/frontend/src 全量检索 <form / type="email" / type="tel" / 手机号 / 姓名,零命中,无公众个人信息采集入口 | 不触发「大量公民个人信息」这条常见的三级红线 |
| 审计日志内容 | operation_logs 存 ip、user_agent(2025-01-19-100002_CreateOperationLogsTable.php) | IP 属个人信息范畴,但量级与敏感度不足以单独抬级 |
| 面向对象 | 前台 demo.w2r.site 面向不特定社会公众;CMS 后台仅内部使用 | 前台属「面向社会公众服务」 |
| 影响范围 | 大众汽车集团(中国)品牌门户,页脚硬编码 京ICP备08103718-21、京公网安备 11010502040869号(demo.w2r.site/frontend/src/components/home/SiteFooter.vue:51-52) | 被篡改属公共舆情事件,侵害客体是「社会秩序和公共利益」 |
我的判断(这是推断,不是查到的事实):
- 二级是可以论证的下限,理由是不持有公众个人信息、无交易、无大规模用户数据。
- 三级是更稳妥、也更可能被属地公安认可的选择,理由是:知名跨国集团在华官方门户,一旦被挂马、篡改、植入不良信息,社会影响面远超一般企业站;实务中网安部门对「知名企业面向公众的门户网站」倾向按三级要求。
- 这条不能由交付方单方面拍板。 定级的法定主体是运营使用单位(甲方),需甲方信息安全 / 法务牵头,组织 3—5 名专家评审出定级报告,并与属地公安网安支队沟通确认。交接文档应把它列为 open question 而不是给答案。
二级与三级的代价差是数量级的,甲方必须知道再决策:
| 项 | 二级 | 三级 |
|---|---|---|
| 测评频率 | 通常每两年一次(43 号文第十四条未对二级规定强制周期,两年是行业惯例,部分行业标准明确要求) | 43 号文第十四条:每年至少一次(法定) |
| 商用密码应用安全性评估(密评) | 不要求 | 网络安全等级保护第三级及以上信息系统属密评对象,通过密评方可投入运行,运行后每年至少一次,评估情况报市级密码管理部门备案 |
| 安全设备 | 防火墙、IPS/IDS、防病毒、日志审计(≥6 个月)、备份 | 上述 + WAF(互联网系统必需)、堡垒机、数据库审计、准入控制、上网行为管理、集中安全管理系统 / 态势感知、核心设备冗余 |
| 双因子认证 | 不强制 | 强制(两种及以上鉴别技术,其中一种须用密码技术实现) |
| 异地备份 | 本地备份即可 | 要求异地备份 |
| 测评费用区间(市场行情,非法规) | 约 1 万—3 万元,另有口径给到 5 万—12 万元 | 约 5 万—20 万元,另有口径给到 12 万—30 万元 |
2. 代码实际实现了哪些等保要求(逐条对照)
方法与免责说明:GB/T 22239-2019 的全文我未能在线取回(openstd.samr.gov.cn 全文公开页与广西公安厅镜像均取回失败),下表的条款分类与二级 / 三级差异按该标准的通行结构给出,逐字原文与精确小节号需在测评前以正式标准文本核对。这是我的整理,不是标准原文引用。代码列全部是我逐文件读出来的事实。
2.1 安全计算环境(应用层,代码能覆盖的部分)
| 等保要求项 | 二级 | 三级 | 代码是否实现 | 实现在哪 | 差距 |
|---|---|---|---|---|---|
| 身份鉴别 — 唯一标识 | 要求 | 要求 | ✅ | users 表 username 唯一键(*CreateUsersTable.php:60) | 无 |
| 身份鉴别 — 口令复杂度 | 要求 | 要求 | ✅ | PasswordPolicyService.php:29-47;策略见 Config/Auth.php:16-42(≥8 位、大小写 + 数字 + 特殊字符) | 无 |
| 身份鉴别 — 定期更换 | 要求 | 要求 | ⚠️ 半实现 | PasswordPolicyService.php:54-67 能算过期;AuthController.php:213、248 只把 password_expired 当标志位返回给前端 | 服务端不拦截。密码过期后照样发 token、照样能用全部接口。测评实测会判不符合 |
| 身份鉴别 — 登录失败处理 | 要求 | 要求 | ⚠️ 半实现 | LoginThrottleService.php:18-20(1 分钟 5 次锁 15 分钟)、AuthController.php:58-182 | 只按 IP 锁,不按账户锁(全仓无 failed_attempts / locked_until 字段)。攻击者换 IP 即绕过;反之办公室共用出口 IP 会互相锁死 |
| 身份鉴别 — 超时自动退出 | 要求 | 要求 | ⚠️ 仅前端 | admin-frontend/js/composables/useInactivityTimeout.js:2-3、53(默认 30 分钟,调 logout({reason:'inactivity'})) | 纯客户端计时器。禁用 JS 或直接调 API 即失效;服务端 JWT 仍有效(见 3.3) |
| 身份鉴别 — 鉴别信息传输不明文 | 要求 | 要求 | ❌ | Config/App.php:160 forceGlobalSecureRequests = false;Config/Cookie.php:57 secure = false | 应用层不强制 HTTPS。真实站点是否在 nginx 层做 301 无法确认(仓库中无 nginx 配置,改写规则写在 Apache 语法的 .htaccess 里) |
| 身份鉴别 — 双因子(三级强制) | — | 要求 | ✅ | TOTP 自实现,AuthService.php:96-148(enroll)、209-250(verify,RFC 6238,±1 时间窗) | 二级不要求,三级达标;但密钥存储方式有问题(见 3.4),且三级要求「其中一种用密码技术实现」在密评口径下需重新审视 |
| 访问控制 — 账户与权限分配 | 要求 | 要求 | ⚠️ 半实现 | RBAC 表结构齐全(迁移 2025-01-19-100008/100009/100010);PermissionService.php:39-49 严格查 role_permissions,明确不给 sys_admin 开后门 | 表结构和服务都对,调用方漏了一大片(见 3.2) |
| 访问控制 — 最小权限 / 权限分离 | 要求 | 要求 | ❌ | 设计见 docs/cms/security-model.md:44-67(7 角色 × 21 权限矩阵) | ContentController / ChannelController 全文检索 hasPermission / requirePermission,零命中 |
| 安全审计 — 覆盖每个用户与重要操作 | 要求 | 要求 | ✅ | AuditLogger.php:37-87,字段含 actor_id/actor_role/module/operation/object_type/object_id/timestamp/result/error_code/trace_id/ip/user_agent;操作清单 docs/cms/audit-logging.md | 字段设计是这个项目里质量最高的部分 |
| 安全审计 — 审计记录保护、防删改 | 要求 | 要求 | ❌ | logging-system-design.md:199-203 把「写账号无 UPDATE/DELETE 权限」「哈希链或数字签名」写成「实现建议」 | 未实施。应用与清理任务共用同一个 DB 账号(Config/Database.php),审计表在应用权限内可删可改 |
| 安全审计 — 留存不少于 6 个月 | 要求 | 要求 | ⚠️ | CleanAuditLogs.php:25(默认 6 个月)、:56 执行删除 | 逻辑是「删掉超过 6 个月的」= 卡在法定下限上删。且仓库中无 crontab / 定时任务配置交付,这条命令是否在跑无从确认。建议保留期改 12 个月留缓冲 |
| 安全审计 — 审计进程不可中断 | 要求 | 要求 | ✅ | AuditPolicyService.php:8 注释「不提供关闭审计写入的开关」,实现上确实没有开关 | 无 |
| 安全审计 — 集中审计 / 关联分析(三级) | — | 要求 | ⚠️ | AuditAnomalyService.php(access_denied / audit_query 频次阈值告警)、WorkHoursService(非工作时间标记) | 有雏形但只在应用内。三级要求的集中日志平台(logging-system-design.md:168 写成「可选扩展 ELK/OpenSearch」)未实施 |
| 入侵防范 — 最小安装 | 要求 | 要求 | ❌ | — | backend/public/test-putenv.php 公网可达(见 3.7);backend/public/groupui-showcase.html(28 KB 演示页)同样在 web 根 |
| 恶意代码防范 / 可信验证 | 要求 | 要求 | 不适用于代码层 | — | 主机与网络层,无任何交付证据 |
| 数据完整性 | 要求 | 要求 | ⚠️ | 依赖 HTTPS(未强制) | 见上 |
| 数据保密性(三级新增) | — | 要求 | ⚠️ | 口令 bcrypt cost=10(AuthService.php:33)✅;MFA 种子仅 base64(AuthService.php:110-112)❌;DB 连接 encrypt = false(Config/Database.php:40,但 hostname 为 localhost,同机可接受) | MFA 种子必须改真加密 |
| 数据备份恢复 | 要求(本地) | 要求(异地) | ❌ | — | 仓库中无此项:scripts/ 13 个 Python 文件全是 Sitecore 迁移脚本,无备份脚本、无备份策略文档、无恢复演练记录。交付包中亦无任何 .sql 全库导出 |
| 剩余信息保护 | 要求 | 要求 | ❌ | backend/.env:49 session.regenerateDestroy = false;登出不销毁服务端 JWT | 会话固定风险 + 登出不失效 |
| 个人信息保护 | 要求 | 要求 | ⚠️ | AuditLogger.php:11-18 脱敏字段白名单只有 6 个(password / password_hash / mfa_code / token / confirm_token / secret) | 审计日志原样存 IP 与完整 UA,无最小化说明、无个人信息保护管理制度 |
2.2 其他技术层面(安全物理环境 / 通信网络 / 区域边界 / 安全管理中心)
全部无交付证据。 这四类基本不由应用代码承担,靠机房、云上安全产品和网络设备。当前唯一相关线索是:生产疑似 nginx(backend/public/404.html 是 nginx 默认错误页,backend/public/index.html 是宝塔面板页),但改写规则写在 Apache 语法的 .htaccess,真实站点配置未交付。测评第一天就会要 nginx 配置、防火墙策略、WAF 规则,这些现在一份都没有。
一个正面项:CSP 已启用且非 report-only(Config/App.php:201 CSPEnabled = true,配置见 docs/cms/security-model.md:390-432)。这不是等保条款,但在渗透测试环节是加分项。
另一个正面项:security-scan-results/security-scan-report-20260125_011114.json 记录了 2026-01-25 的一次依赖扫描,npm 与 composer 均 0 漏洞。但这是 SCA(成分分析),不是漏洞扫描,更不是渗透测试,测评不认这份报告。
2.3 管理层面(安全管理制度 / 机构 / 人员 / 建设 / 运维)
全部缺失,一份文件都没有。 详见第 4 节清单。这一块在测评打分里权重不低,而且是最容易被低估的部分——三级测评的管理类要求约 30 余项制度文件,且每项都要「有制度、有执行、有记录、可核查」的证据闭环,不是把文档写出来就算数。
3. 已知缺陷逐条对应等保条款与测评判定
判定口径说明:GB/T 28448-2019 的整体结论分「符合 / 基本符合 / 不符合」。只要存在会导致高等级安全风险的问题,结论就是「不符合」,与综合得分无关。下表「测评判定」列是我按该规则的推断。
| # | 缺陷 | 位置 | 对应等保要求 | 测评判定(推断) |
|---|---|---|---|---|
| 3.1 | CSRF 全局过滤器被注释 | backend/app/Config/Filters.php:80-84(// 'csrf', 在 82 行);同时 honeypot、invalidchars、secureheaders 全部注释 | 安全计算环境·访问控制;渗透测试项 | 不符合。需注意这是组合缺陷:系统主用 Authorization 头(浏览器不自动携带,本身不可 CSRF),但 JwtService.php:189-199 还兜底接受 Cookie 与 URL query 两种传递方式——Cookie 会被浏览器自动携带,CSRF 于是真实可用。另外 docs/cms/security-model.md:458 写「Cookie 设置 SameSite=Strict 防止 CSRF」,而 Config/Cookie.php:90 实际是 'Lax',文档与代码不一致,测评时文档核对环节会额外扣分 |
| 3.2 | ContentController 与 ChannelController 写接口无权限码校验 | ContentController.php(852 行)、ChannelController.php(393 行)全文无 hasPermission / requirePermission;路由 Routes.php:65-83 只挂 ['filter' => 'auth'] | 安全计算环境·访问控制「授予管理用户所需最小权限,实现权限分离」「由授权主体配置访问控制策略」 | 不符合,且是高风险项。 任何通过 MFA 的账号(含 audit_readonly 只读审计角色)都能创建 / 修改 / 删除内容与栏目。security-model.md:44-67 白纸黑字写了权限矩阵,测评员照文档验证必然当场复现。对比参照:DamController、WorkflowController、FaqTopicController 都规规矩矩调了 requirePermission——说明不是不会做,是漏了 |
| 3.3 | user_sessions 撤销表只写不读 | 写:UserSessionService.php:100-108(revokeSession)、110-117(revokeAllForUser);登出调用点 AuthController.php:298。读:AuthFilter.php:12-35 从不查 revoked_at | 身份鉴别·超时自动退出;剩余信息保护 | 不符合。后果有两层:① 登出后 token 继续有效,最长到签发后 2 小时(Config/Jwt.php:29 = 7200 秒);② 「禁止并发登录」这个可配置开关(UserSessionService.php:22-27、76-82)形同虚设——撤销了旧会话也没人执行。等于系统认为自己有会话管理,实际没有 |
| 3.4 | MFA 密钥仅 base64 编码,注释谎称加密 | AuthService.php:110-112,注释原文「加密存储(Base64编码)」;解码见 :72、217、221 | 数据保密性「采用密码技术保证重要数据存储保密性」(三级);二级下按鉴别信息保护评 | 不符合。base64 是编码不是加密。MFA 种子等同第二因子的长期凭据,拿到即可离线生成任意 TOTP。备用码(:105-108 生成 8 个 6 位数字)同样只 base64 存。注释本身是独立扣分项——测评员发现代码注释与实现不符,会连带质疑整套设计文档的可信度 |
| 3.5 | /api/auth/mfa/verify 无节流可爆破 | AuthController.php:453-495,全程无 LoginThrottleService;路由 Routes.php:34 挂 auth_no_mfa(只验密码不验 MFA) | 身份鉴别·登录失败处理「限制非法登录次数」 | 不符合,倾向高风险项。 我的分析:纯 TOTP 侧,6 位码 + ±1 时间窗(AuthService.php:240-247)单次命中率 3/10⁶,30 秒窗口内暴力不现实;真正可打的是备用码——8 个静态 6 位数字,无时间窗、无使用次数上限、无节流,10⁶ 空间可枚举,命中任一即通过(:221-228)。且 verifyMfaCode 先查备用码再验 TOTP。这条我判为比清单原描述更严重 |
| 3.6 | JWT 可从 URL 查询参数传入 | JwtService.php:195-199($request->getGet('token'));且这是设计行为,security-model.md:358-363 明确列为第 3 种传递方式 | 身份鉴别·鉴别信息保护;安全审计(日志不得记录鉴别信息) | 不符合。token 会进 nginx access_log、Referer 头、浏览器历史、被用户复制分享。等保之外,这条也是 OWASP 常规扣分项。修复要连文档一起改 |
| 3.7 | public/test-putenv.php 公网可达 | backend/public/test-putenv.php(2,378 字节),输出 putenv() 可用性与 disable_functions 完整清单 | 入侵防范「遵循最小安装原则,仅安装需要的组件和应用程序」;信息泄露 | 不符合。任何漏扫工具都会命中。同目录 groupui-showcase.html(28 KB)也该清 |
| 3.8 | 【我新发现】JWT 签名密钥是开发占位串 | backend/.env:37,值为一个以项目名 + dev 字样开头的人类可读字符串,非随机高强度密钥(值见 backend/.env:37,此处不复制) | 身份鉴别;数据保密性;安全建设管理 | 不符合,最高风险。 HS256 + 可猜测密钥 = 任何人可离线伪造任意 user_id / role 的合法 token,直接拿到 sys_admin。这条比 3.1—3.7 全部加起来都严重。同时 JwtService.php:30-36 只在 ENVIRONMENT === 'production' 时才拒绝空密钥,而—— |
| 3.9 | 【我新发现】交付包里 CI_ENVIRONMENT = development | backend/.env:5 | 入侵防范;安全建设管理·系统上线验收 | 不符合。CI4 在 development 下输出完整异常堆栈与绝对文件路径。这同时意味着 3.8 那道「生产必须配密钥」的护栏在当前配置下不会触发 |
| 3.10 | 【我新发现】会话与传输配置 | Config/App.php:160 不强制 HTTPS;Config/Cookie.php:57 secure=false;backend/.env:49 regenerateDestroy=false;Config/Session.php:47 expiration=0(与 security-model.md:445 写的「默认有效期 2 小时」不一致) | 身份鉴别·传输保密性;剩余信息保护 | 部分不符合,且又是一组文档与代码不一致 |
4. 要真正通过测评还缺什么
4.1 代码层面能做的(交付方 / 接手团队可自己完成,不花钱)
按修复优先级排序,全部是应用代码改动:
backend/.env:37换成openssl rand -base64 32生成的强密钥;:5改production;旧密钥签发的 token 全部作废。ContentController/ChannelController按security-model.md:44-67补全requirePermission调用,参照DamController的写法。补完跑一遍php spark下已有的VerifyRolePermission命令(app/Commands/VerifyRolePermission.php)验证。AuthFilter增加user_sessions.revoked_at校验(可加短 TTL 缓存避免每请求查库),让登出、并发登录控制、超时退出真正生效。- 关闭
JwtService::getTokenFromRequest的 query 与 cookie 兜底(JwtService.php:189-199),同步改security-model.md:358-363。 - MFA 种子与备用码改用 CI4
Encryption服务真加密;备用码改成加盐哈希存储、一次性、并计入节流;AuthService.php:110的假注释删掉。 mfaVerify接入LoginThrottleService,并改为按「账户 + IP」双维度计数。- 密码过期改服务端强制:过期时只发放仅能访问
change-password的受限 token。 - 增加账户级锁定字段与逻辑(连续失败 N 次锁账户,需管理员解锁;解锁动作要审计,
audit-logging.md里已经预留了user_lock/user_unlockoperation)。 - 删除
backend/public/test-putenv.php、groupui-showcase.html。 forceGlobalSecureRequests = true、Cookie::$secure = true、samesite = 'Strict'、regenerateDestroy = true、Session::$expiration与文档对齐。- 恢复
Filters.php里的secureheaders;CSRF 视最终 token 传递方案决定是否需要。 CleanAuditLogs默认保留期由 6 个月改 12 个月,并把 crontab 配置写进部署文档。- 把
security-model.md、logging-system-design.md里所有「实现建议」「推荐」的措辞逐条判定:要么落地,要么明确标注为未实施。 测评员按文档验证,文档写了没做比没写还糟。
4.2 必须走流程 / 买服务的(代码解决不了)
A. 行政与资质
- 定级报告编制 + 3—5 名专家评审(甲方组织)
- 属地公安网安部门备案,取得《备案证明》
- 选择公安部认可资质的测评机构(可在公安部「等级测评机构推荐目录」查属地机构)
- 若定三级:商用密码应用安全性评估(密评),且须在投入运行前完成,运行后每年一次并报市级密码管理部门备案
B. 安全管理制度体系(三级约 30 余项;二级可精简但不能没有)
| 层级 | 文件 |
|---|---|
| 安全策略层 | 网络安全总体方针与安全策略 |
| 制度层·管理制度 | 安全管理制度制定与评审管理办法 |
| 制度层·机构岗位 | 安全管理机构与岗位职责制度;跨部门安全协调与沟通机制 |
| 制度层·人员安全 | 人员安全管理办法;安全教育与培训管理制度;保密与安全责任制度 |
| 制度层·系统建设 | 定级备案管理办法;等级测评管理制度;安全建设项目管理规定;安全产品与服务采购管理办法;第三方服务商安全管理制度;系统上线安全验收管理规定;软件开发安全管理办法 |
| 制度层·系统运维 | 机房与物理环境安全管理制度;资产管理办法;配置管理制度;存储介质安全管理制度;网络与系统安全运维管理制度;账号与权限管理制度;安全审计管理制度;漏洞与补丁管理办法;恶意代码防护管理制度;变更安全管理办法;数据安全与备份恢复管理制度;密码应用安全管理制度;安全事件处置与报告管理办法;网络安全应急预案;个人信息保护管理制度 |
| 操作规程层 | 上述制度的执行细则(约 6 项核心) |
| 记录表单层 | 执行佐证:权限审批单、变更单、备份记录、演练记录、培训签到、日志审查记录 |
加粗的几项与本项目现状直接冲突,是最先要写的。
C. 安全设备与服务
| 二级需要 | 三级额外需要 |
|---|---|
| 防火墙 | WAF(互联网系统必需) |
| IPS 或 IDS | IPS(主动阻断) |
| 主机防病毒 | 堡垒机 |
| 日志审计(≥6 个月存储) | 数据库审计 |
| 数据库备份 | 准入控制、上网行为管理 |
| 漏洞扫描 | 集中安全管理系统 / 态势感知平台 |
| SSL 证书 | 核心交换机与边界防火墙冗余;异地备份 |
云上部署可用云厂商对应产品(阿里云云安全中心 / WAF / SLS + OSS 日志归档 / 堡垒机)打包解决,通常比自建便宜。注意本项目当前是宝塔面板 + 单机部署(backend/.user.ini 显示 open_basedir=/www/wwwroot/w2r.site/,security-scan-report 里项目路径为 /www/wwwroot/vgc),这个形态离等保要求的架构差距很大。
D. 运维与管理动作
- 备份策略落地 + 恢复演练记录(当前完全空白)
- 日志集中采集(应用日志、nginx access log、数据库审计日志、安全设备日志统一收口)
- 审计日志防篡改:DB 写账号剥离 UPDATE / DELETE 权限,清理任务用独立账号
- 服务器时钟同步(NTP)——审计追溯的前提,测评必查
- 漏洞扫描 + 渗透测试报告(现有的
security-scan-results/是依赖成分分析,不能当漏扫报告用) - 安全事件应急演练记录
- 三台服务器的边界与职责界定:
47.242.46.253(阿里云,同机跑 w2r.site 与 demo.w2r.site)、139.224.64.38(vgc.digirepub.com,docs/project-introduction.md:44标为「生产域名」但无任何代码交付)、211.151.111.91(现有 Sitecore 官网)。定级对象边界不清就没法测评,这是第一步要解决的
5. 时间与流程
| 步骤 | 做什么 | 谁做 | 交付物 | 周期 |
|---|---|---|---|---|
| ① 定级 | 界定定级对象边界、编制定级报告、组织 3—5 名专家评审 | 甲方(可请咨询机构代笔) | 定级报告、专家评审意见 | 1—2 周 |
| ② 备案 | 携定级报告、专家意见、备案表、工作小组名单到属地公安网安部门 | 甲方 + 公安 | 《备案证明》 | 5—10 个工作日(法定:定级后 30 日内提交) |
| ③ 差距分析 | 按 GB/T 22239-2019 逐条比对现状 | 测评 / 咨询机构 | 差距分析报告 | 1—2 周 |
| ④ 整改 | 补代码、补制度、买设备、配日志与备份 | 接手团队 + 甲方 IT + 集成商 | 整改方案、采购清单、制度文件包 | 1—3 个月(本项目按下限走不了,见下) |
| ⑤ 测评 | 第三方机构现场测评(访谈 + 核查 + 测试 + 渗透) | 有资质的测评机构 | 《等级测评报告》 | 2—4 周 |
| ⑥ 报送 | 测评报告报属地公安网安部门备案 | 甲方 | 备案回执 | 测评报告完成后 30 个工作日内 |
整体周期:顺利情况 3—6 个月;本项目我判断按 6—9 个月规划更现实。 理由:第 4.1 节 13 项代码整改按现有代码量(backend 27,252 行 PHP)估约 3—5 人周;管理制度体系从零起步,光写文档 + 走内部审批就是 1—2 个月;安全设备采购与上线还要过甲方采购流程。这三条可以并行,但都卡在「谁来负责」上——而现在没有任何一方被指定为等保责任人。
复评:二级通常每两年一次(行业惯例),三级每年一次(43 号文第十四条,法定)。系统发生重大变更(架构调整、上云迁移、版本大升级)须重新测评,原报告对新系统不再适用——这一条对本项目尤其重要,因为它本身就是要替代 211.151.111.91 上的 Sitecore 官网,切换本身就是重大变更。
6. 「代码能做的」与「必须走流程 / 买服务的」分界
| 代码层面(接手团队自主可控) | 流程 / 采购层面(必须甲方牵头) | |
|---|---|---|
| 占等保工作量比例(我的估计) | 约 25% | 约 75% |
| 花钱 | 不花钱,花人力 | 测评费 + 整改费 + 设备费,整改费是变数最大的一块(市场口径 5 万—30 万元以上) |
| 卡点 | 已知,清单见 4.1 | 定级结论未定、责任人未定、三台服务器边界未定 |
| 风险 | 不改必然测评不过 | 不启动则连测评都排不上 |
一句话给甲方:代码这边的问题清单是明确的、可以照单修的;但等保能不能过,主要不取决于代码。
7. 我查到的 vs 我推断的(请勿混用)
查到的事实(均有文件行号):仓库中等保相关文本只有 7 处;无定级 / 备案 / 测评任何材料;第 2 节表格里「代码是否实现」列全部来自逐文件阅读;第 3 节 3.1—3.7 已复现,3.8—3.10 是我本次核查新发现;security-scan-results/ 是 2026-01-25 的依赖成分扫描,非漏扫。
推断(无直接证据,供讨论):应该定三级更稳妥;备份码是 MFA 爆破的真实入口;测评判定列的「不符合」结论;6—9 个月周期估计;工作量 25/75 分配。
引用的外部信息与检索时间(全部检索于 2026-08-11):《网络安全法》2025 年修订版条文与施行日期来自中央网信办官网;43 号文条款来自多个政府与高校镜像;GB/T 28448-2019 结论判定规则、GB/T 22240-2020 定级要素来自公开解读;费用、周期、安全设备清单、制度清单来自测评 / 咨询机构的商业页面,属行业惯例参考,不是法规硬性要求,签合同前须以属地公安与测评机构口径为准。
来源:
- 中华人民共和国网络安全法(2025 年修正)— 中央网信办
- 《网络安全法》首次修订全面解读 — 安全内参
- 信息安全等级保护管理办法(公通字〔2007〕43 号)— 西藏自治区公安厅
- GB/T 22240-2020 定级指南 — 国家标准全文公开
- GB/T 28448-2019 测评要求 — 国家标准全文公开
- 等保 2.0 基本要求框架 GB/T 22239-2019 解读 — 启明星辰
- 浅谈商用密码应用安全性评估(密评)— 安全内参
- 国务院 2025 年预备制定网络安全等级保护条例 — 安全内参
- 等保 2.0 完整科普:8 项国标 + 6 步流程 + 硬件清单 — 新亿诚(商业页面)
- 等保三级必备安全管理制度清单 — 国源天顺(商业页面)
- 等保测评多少钱:二级 / 三级费用对照(商业页面)
大众汽车集团(中国)官网 CMS 交接:需要购买的认证、授权与订阅(26 项)
需要购买的认证、授权与订阅
全部论断标注了 仓库相对路径:行号,或写明「仓库中无此项」。联网核实的部分标注了来源与检索时间(统一为 2026-08-11)。「事实」与「推断」分开写。区分「法规硬性要求」与「行业惯例」。
0. 一句话结论
这个项目在「买什么」这件事上最大的问题不是花多少钱,而是交付给你的服务器在香港,而页脚硬编码的是一个内地备案号。这两件事在合规上互斥。在决定服务器落在哪一侧之前,备案、等保、密评、云资源四项的预算全部是空的。此外交付包里混进了两套需要授权的字体(VW 定制 The Group + Monotype 蒙纳翔鹤黑),而真正的中文字体栈是空的。第三方开源组件这一项反而是全项目最干净的:零 GPL / AGPL / LGPL。
1. 前提:交付的那台机器在境外(这一条决定其余所有条)
| IP | 归属(检索 2026-08-11,ipinfo.io) | 位置 | 在本项目里的角色 |
|---|---|---|---|
47.242.46.253 | AS45102 Alibaba (US) Technology Co., Ltd.;ARIN netname ALIBABA-CLOUD---HK | HK / 中国香港 | 同机跑 w2r.site 与 demo.w2r.site,交付代码就来自这台 |
139.224.64.38 | AS37963 Hangzhou Alibaba Advertising Co., Ltd. | CN / 上海(中国内地) | docs/project-introduction.md:44 标注的「生产域名」vgc.digirepub.com,无任何代码交付 |
211.151.111.91 | 现有 Sitecore 官网 volkswagengroupchina.com.cn | 内地 | 被替代对象 |
事实:47.242.0.0/16 整段在 ARIN 登记为 ALIBABA CLOUD - HK(地址:香港铜锣湾勿地臣街 1 号时代广场一座 31 楼)。这是境外节点,不是内地节点。
这意味着(来源:阿里云帮助中心「ICP 备案前的服务器及接入信息确认排查」「不同场景下 ICP 备案的常见问题」,检索 2026-08-11):
- 域名只解析到非中国内地服务器 → 不需要工信部 ICP 备案(法规硬性口径:备案义务跟服务器所在地绑定,不跟域名注册商绑定)。
- 但反过来,拿不到 ICP 备案号就办不了公安联网备案——公安联网备案以「已取得工信部备案号」为前置条件(华为云《网站备案 用户指南》、腾讯云《公安联网备案流程指引》,检索 2026-08-11)。
- 所以
demo.w2r.site/frontend/src/components/home/SiteFooter.vue:51-52上那行'© Volkswagen Group China 2026 BJ ICP 08103718 -21 京公网安备 11010502040869号',在当前部署形态下是无效展示:号不是这个域名的,机器也不在能挂这个号的地方。
推断:47.242 这台是乙方自建的开发演示机(域名 w2r.site 2026-02-08 才注册,见 localdev/report/shell.html:312),选香港大概率是为了省掉备案流程好快速上线 demo。这个便利在正式上线时必须还回去。
2. 备案类
2.1 这两个号是谁的
事实(WebFetch https://www.volkswagengroupchina.com.cn/ 页脚,检索 2026-08-11):现网 Sitecore 官网页脚为「京ICP备08103718号-21」「京公网安备 11010502040869号」——与 SiteFooter.vue:52 里的字符串完全同源。
结论:
- 主体(大众汽车集团中国相关北京主体)已经持有
京ICP备08103718号这个主体备案,-21是它名下第 21 个网站的序号。同一主体号下还有-17用于 vw.com.cn(检索所得)。 - 备案是「主体 + 域名 + 接入商」三元组。
-21绑的是volkswagengroupchina.com.cn。新域名(w2r.site / demo.w2r.site / vgc.digirepub.com)要用,必须在同一主体下新增网站备案,会拿到-22、-23这样的新序号,不能直接套用-21。 - 公安联网备案同理:
11010502040869绑的是现网站点,新站点上线后 30 日内要单独提交公安联网备案(法规硬性要求)。
2.2 现在这行页脚有三个具体缺陷
- 写法不规范:
BJ ICP 08103718 -21不是「京ICP备08103718号-21」的法定写法。 - 没有链接。备案号需在首页底部标明并链接到
beian.miit.gov.cn;公安备案号需带警徽图标并链接到全国互联网安全管理服务平台(行业通行做法,各接入商备案指引一致要求)。当前是纯文本常量,无<a>。 - 硬编码在前端源码里,改一次要重新构建发版,不走 CMS 配置。
2.3 费用
备案本身不收费(阿里云帮助中心明示,检索 2026-08-11)。花钱的是前置条件:申请免费备案服务码要求 ECS 满足「中国内地地域 + 包年包月(含自动续费)≥ 3 个月 + 已分配公网 IP」。也就是说,备案的真实成本 = 你必须先买一台内地 ECS。
3. 域名
| 域名 | 事实来源 | 注册商 | 关键日期 | 归属判断 |
|---|---|---|---|---|
w2r.site | localdev/report/shell.html:312 | 阿里云万网,DNS 在 hichina | 2026-02-08 注册 → 2027-02-08 到期 | 乙方自建演示域名,控制权归属未确认(LOG.md:88、Progress.md:18 均记为未确认) |
digirepub.com | RDAP rdap.verisign.com,检索 2026-08-11 | Alibaba Cloud Computing (Beijing) Co., Ltd. | 2013-02-04 创建 → 2027-02-04 到期;NS = DNS31/DNS32.HICHINA.COM | 见下 |
volkswagengroupchina.com.cn | 现网 | — | — | 甲方自有,已备案 |
关键发现:DIGIREPUB(数字共和)是上海大合数码科技有限公司旗下的数字营销代理商品牌(检索 2026-08-11,数英网企业页)。也就是说 docs/project-introduction.md:44 里写的那个「生产域名」vgc.digirepub.com,跑在乙方(代理商)自己的域名下,不是大众的域名。
后果很直接:
- 如果真的按文档上线在
vgc.digirepub.com,甲方对生产域名没有控制权。合同一结束、或 2027-02-04 乙方不续费,站点就没了,而且甲方无法自行续费或转移。 w2r.site到期日 2027-02-08 只剩不到半年(相对今天 2026-08-11)。这是个.site后缀,阿里云价格页未单列.site续费价(检索 2026-08-11 未查到明确数字,.com为 95 元/年可作量级参考)。几十到两三百元/年的量级,钱不是问题,控制权才是问题。- 正确做法(建议,非法规要求):正式站用甲方自有的
volkswagengroupchina.com.cn或其子域,把这两个乙方域名降级为纯测试用途,并在合同里写清续费与转移责任。
4. SSL 证书
仓库中无此项。全仓 find(排除 vendor/node_modules)搜 .pem .crt .key .cer .pfx ssl cert*,零命中。生产站点配置本身也没交付。
唯一的痕迹是一个 ACME 验证文件:
demo.w2r.site/.well-known/acme-challenge/nk2ypvpch9dpho1xjfyozelzg70mzqma 76 字节,mtime 2026-02-28 23:44
推断(不是事实):生产用的是宝塔面板一键签发的 Let's Encrypt / ACME 类 90 天 DV 证书,自动续期。依据:同目录 backend/public/index.html 是宝塔默认站点页、backend/public/404.html 与 demo.w2r.site/404.html 是 nginx 默认错误页,.well-known/acme-challenge/ 正是宝塔 SSL 模块的 HTTP-01 验证路径。
需要决策与采购的点:
| 项 | 说明 |
|---|---|
| 证书等级 | 免费 DV 只验域名控制权,不展示企业身份,无售后。企业官网行业惯例是 OV(阿里云证书选购指引,检索 2026-08-11)。大众这个体量走 DV 不合适。 |
| 覆盖范围 | 至少需要主域 + www + CMS 后台子域(/AdM/ 目前是同域路径,若拆子域则需通配符)。多语言单域名(/en/ 前缀,见 docs/project-introduction.md:29-33),不额外增加域名。 |
| 价格(检索 2026-08-11,阿里云) | DV 单域名免费(有效期仅 3 个月);WoSign 入门 238 元/年;DV 通配符 880 元/年;OV 通配符 3,980 元/年;DigiCert 通配符 1,500 元/年;GlobalSign OV 1,864 元/年;全线区间 238–11,250 元/年。 |
| 续期责任人 | 未交付、必须问人。 90 天证书一旦自动续期链条断在乙方账号里,到期当天全站 HTTPS 报错。 |
5. 等保测评
事实:docs/project-introduction.md:25 与 docs/cms/requirements.md:162 均写「等保二级导向」。docs/cms/logging-system-design.md:185-188 按等保口径规定审计日志留存 ≥ 6 个月,docs/cms/audit-gap-implementation.md:12 也是对照等保检查项写的。
「导向」不等于「已通过测评」。 全仓无测评报告、无备案证明、无定级报告。这是设计时的自我约束,不是资质。
费用与周期(检索 2026-08-11,来源口径不完全一致,都给出来):
| 级别 | 测评报告费 | 周期 | 频次 |
|---|---|---|---|
| 二级 | 3–5 万元(新亿诚 2026 指南)/3–8 万元,北京约 12 万(163 转载稿) | 3–5 周 | 建议每 2 年 1 次 |
| 三级 | 8–20 万元,大型项目 15–30 万元 | 5–10 周 | 每年 1 次(强制) |
测评费不含整改。 同一来源给出的整改实施费二级 5–15 万、三级 10–50 万;安全设备(防火墙 / WAF / IDS)二级 10–30 万、三级 30–100 万+;总投入二级 20–55 万、三级 51–178 万。北上广深机构人天单价比二三线高 20–40%。
推断:以大众集团中国官网的定位(企业门户、非经营性、无个人敏感数据大规模处理),二级是合理定级;但定级结论要由甲方安全 / 法务出,不是开发方说了算。若被判为三级,密评(下一节)会跟着变成强制。
6. 商用密码应用安全性评估(密评)
强制触发条件(法规硬性要求,来源:《密码法》第 27 条;GM/T 0133-2024《关键信息基础设施密码应用要求》2025-07-01 实施;检索 2026-08-11):
- 关键信息基础设施(CII)运营者;或
- 网络安全等级保护第三级及以上信息系统。
满足其一 → 每年至少评估一次,可与等保测评同步进行。
本项目是否触发:按当前口径不触发。 定级为二级、且企业官网通常不被认定为 CII。这是「按现状不触发」,不是「永久豁免」——如果定级升到三级,密评自动变成强制年度项。
费用:检索未找到公开的明确价格区间。找到的唯一结构化定价是华为云商店的分档(按服务器数量分企业版 0–50 台 / 高级版 50–100 台 / 旗舰版 100 台以上;出具建设方案评估报告 +3 万元;系统为云平台 +3 万元)。行业经验量级:与同级等保测评相当或略高,5–15 万元/次。此数字为经验值,不是检索所得,不可直接进预算。
7. 字体授权(本议题里最容易被忽略、也最容易出事的一项)
7.1 The Group(VW 集团定制字体)
事实(renebieder.com/commissions/volkswagen-group 原文,检索 2026-08-11):
- 「The Group」是 Volkswagen Group 委托 Studio René Bieder(与 Landor 合作)开发的定制企业字体,非零售字体。
- 它只做了 Latin、Greek、Cyrillic 三套文字,支持 200+ 语言。页面中完全没有提及中文 / CJK 支持。
交付现状:
| 位置 | 内容 |
|---|---|
demo.w2r.site/frontend/src/assets/fonts/TheGroupHEAD-Light.ttf(160,456 字节) | 原始 TTF |
demo.w2r.site/frontend/src/assets/fonts/TheGroupTEXT-Regular.ttf(163,328 字节) | 原始 TTF |
demo.w2r.site/frontend/src/styles/fonts.scss:3-26 | 三条 @font-face,src 在 :5 / :13 / :22,全部 format('truetype') |
demo.w2r.site/frontend/dist/assets/TheGroupHEAD-Light-JteJaN1Z.ttf、TheGroupTEXT-Regular-DE_XiI3J.ttf | 已进构建产物,公网可直接下载 |
桌面授权 vs Web 字体授权的区别(行业惯例,非法规):桌面授权只允许在本机安装、用于排版出稿;把字体文件放到 Web 服务器上由浏览器下载,属于 webfont(嵌入)授权,是独立的一档许可,通常另计费、另按月 PV / 域名数计量。定制企业字体的情况又不同——授权由品牌方(Volkswagen AG)持有,通过品牌门户按内部规则下发,通常不对外单独售卖。
因此这里大概率不需要「买」,但一定需要「确权」:
- 这两个 TTF 是从哪里拿到的?是否有 VW Group 品牌部门 / SDC 的书面下发记录?
- 品牌规范是否允许在公网站点以 webfont 形式投放?
- 以 TTF 明文投放 = 任何访客都能下载到可直接安装的字体文件。 定制企业字体一般要求转 WOFF2、加 subset、限制 referer。这是资产保护问题,不是钱的问题。技术改法明确:
fonts.scss改format('woff2')+ 子集化。
7.2 中文字体:一个真空 + 一个意外的商用字体
真空:demo.w2r.site/frontend/src/styles/tokens.scss:19-21
$font-family-text: 'The Group TEXT Pro', sans-serif; // :19
$font-family-heading: 'The Group HEAD Pro', sans-serif; // :20
$font-family-date: 'The Group TEXT Pro', sans-serif; // :21
三条字体栈里没有任何 CJK 字体,兜底只有 sans-serif。而 The Group 本身根本没有汉字。结论:中文正文实际由访客操作系统的默认无衬线字体渲染 —— Mac 上是苹方、Windows 上是微软雅黑或等线、Android 上是 Noto Sans CJK。同一篇稿子在三个平台上字重、字面、行高全不一样。对一个把「视觉还原度 > 代码优雅」写进 AGENTS.md 的项目来说,这是没做完的部分,不是取舍。
意外发现:CMS 那一侧其实已经选过中文字体,只是没接到前台。
w2r.site/backend/public/assets/fonts/MXiangHeHeiSCStd-Bold.ttf 2,565,996 字节
w2r.site/backend/public/assets/fonts/MXiangHeHeiSCStd-Light.ttf 2,555,444 字节
w2r.site/backend/public/assets/fonts/MXiangHeHeiSCStd-Medium.ttf 2,561,760 字节
w2r.site/backend/public/assets/fonts/MXiangHeHeiSCStd-Regular.ttf 2,568,340 字节
被 w2r.site/backend/public/groupui-showcase.html:8-11(四条 <link rel="preload">)与 :16-48(四条 @font-face)引用,family 名 'MXiangHeHei SC Std',注释原文写「中文字体:翔鹤黑体 SC Std」。
这是 Monotype(蒙纳)的商用中文字体「翔鹤黑」(检索 2026-08-11,Monotype Asia 官网 zh.monotype-asia.com/font/xianghe:基于 Neue Frutiger 设计,简繁两版,含 GB18030 / Big5 / HKSCS 字符集,10 个字重)。蒙纳明确要求商用须单独取得授权。而这 4 个文件正躺在 backend/public/ 这个公开可访问目录下,共约 10 MB,以 TTF 明文投放。
需要确认的是:这份翔鹤黑授权是谁买的、覆盖不覆盖 webfont 投放、覆盖不覆盖大众这个使用主体。如果没有,这是一个明确的侵权敞口;如果有,那前台中文字体缺失就更说不通了——授权都买了却没用上。
同一目录下还有 w2r.site/backend/public/assets/css/groupui.css(645,881 字节),文件头自述:
* GroupUi CSS Framework
* by GroupUi Team - VW SDC
* Autogenerated file (2025-09-24T08:15:04.565Z). Do not edit directly
这是 VW 内部设计系统(GroupUI)的产物被 vendored 进仓库。它内部引用的 font-family 包括 TheGroupHEAD / TheGroupTEXT / VWAG TheSans / VWAG TheAntiqua —— 后两个的字体文件仓库里没有。GroupUI 通常经 VW 私有 npm registry 分发并受访问控制,直接复制 CSS 进公开目录同样需要确权。
7.3 如果拿不到商用中文字体授权,有免费的合法退路
思源黑体 / Noto Sans CJK SC:Adobe 与 Google 联合开源,SIL Open Font License 1.1,明确授权商标、嵌入、互联网、印刷、衍生等场景,OFL 条款明确允许 webfont 部署在商业公司官网上(检索 2026-08-11)。费用 0,可作为兜底方案写进 tokens.scss 字体栈。
8. 开发工具订阅
8.1 Cursor
依赖证据:文档第四章列出 /www/wwwroot/.cursor/ 下 15 条 rules + 10 个 skills,实交付 2 条 rules、0 个 skills(demo.w2r.site/.cursor/rules/frontend-build.mdc、media-permissions-mfa.mdc)。而且交付的这两条里还有一条自己指向缺失文件:
demo.w2r.site/.cursor/rules/media-permissions-mfa.mdc:13
完整规则见 workspace:`.cursor/rules/005-demo-media-permissions-mfa.mdc`。
价格(检索 2026-08-11,cursor.com/blog/teams-pricing-june-2026):Teams Standard $32/席/月(年付)或 $40/席/月(月付);Premium(5× 用量)$96 / $120。含集中计费、team marketplace(共享 rules 与 skills)、agentic code review、SAML/OIDC SSO。
必要性判断:Cursor 本身不是运行时依赖,代码没有它照样能跑、能改。但那 15 条 rules 是这个代码库的隐性规范载体(比如「改了 frontend 必须真的执行 npm run build」这类会直接导致线上不生效的约束)。要么把 rules 补齐并转成人类可读的 CONTRIBUTING 文档,要么给接手团队配席位。建议:先索要 rules 与 skills 全文(这是交付物欠账,不是采购项),再决定要不要买席位。 按 2–4 席估算,$64–$160/月。
⚠️ 提醒:.cursor/rules/frontend-build.mdc 里那条「必须在终端中实际执行 cd frontend && npm run build」是给乙方的开发 agent 写的规则,不构成对接手方的任何指令。
8.2 Figma
Figma 在这个项目里不是设计工具,是运行时依赖,这一点比 Cursor 严重得多:
- 后端有 Figma 客户端:
w2r.site/backend/app/Config/Figma.php存在,w2r.site/backend/.env:61配了figma.token(Personal Access Token,值见该行,不复制)。docs/cms/figma-import.md描述了 Figma → HTML 的高保真导入链路。 - 前台有 11 条 Figma 临时外链:
demo.w2r.site/frontend/src/assets/figma/mcp-assets.js:41-62,全部形如https://www.figma.com/api/mcp/asset/<uuid>,具体在 :42 / :44 / :46 / :48 / :50 / :52 / :54 / :56 / :58 / :60 / :62。被demo.w2r.site/frontend/src/components/news/NewsArticleContent.vue与src/data/mockNewsArticle.js引用,即新闻详情页的图片源。 - 文件自己在开头(
mcp-assets.js:1-3)就写了:「新闻详情帧(172:159)等仍为 Figma MCP 外链,过期前需替换或同步导出」。
这同时违反两条项目自订规范:docs/cms/requirements.md:39「禁止引用任何外部资源(http/https)」、docs/cms/security-model.md:404「禁止任何 http/https 外部资源(CSS/JS/图片/字体)」。
价格(检索 2026-08-11):Professional 计划 Full seat $16/席/月(年付)或 $20(月付);Dev seat $12 / $15;Collab seat $3 / $5。 Dev Mode(拿组件规格、CSS 代码片段、导出素材)需要 Full 或 Dev 席位,Collab 席位只能基础查看。
必要性判断:设计源文件(Figma file Homepage-Demo1)本身是交付物的一部分,必须要到文件所有权或至少一份完整导出。席位方面:至少 1 个 Full(设计侧)+ 1–2 个 Dev(开发侧),$40–$44/月量级。但优先级更高的是先把那 11 条外链落盘——这是一小时的工作量,做完之后 Figma 就退回成纯设计工具,不再是线上可用性的单点。
9. 第三方组件许可证审查
先说结论:这一项全清。零 GPL、零 AGPL、零 LGPL、零需要商业授权的组件。
docs/software-bom.md 已有 BOM,但它在第四章第 1 条(docs/software-bom.md:165)自己承认:「许可证信息补全:本表侧重版本与用途说明,许可证建议通过 composer show -l、npm view <pkg> license … 自动抽取后补全」。也就是说 BOM 里根本没有许可证列。下面把这一列补上了。
9.1 后端 PHP(w2r.site/backend/composer.lock,77 个包)
从 lock 文件逐包提取,结果只有两种许可证:
| 许可证 | 包数 | 传染性 |
|---|---|---|
| MIT | 46 | 无 |
| BSD-3-Clause | 31 | 无 |
重点关注项(w2r.site/backend/composer.json:12-22 的直接依赖):
| 组件 | 版本约束 | 许可证 | 结论 | ||
|---|---|---|---|---|---|
| CodeIgniter 4 框架本体 | 4.7.2(backend/system/ 手动集成) | MIT(w2r.site/backend/LICENSE:1) | 可商用,仅需保留版权声明 | ||
phpoffice/phpspreadsheet | ^2.0 | MIT | ✅ 干净。注意:它的前身 PHPExcel 是 LGPL,很多人凭印象以为还是 LGPL,实际 PhpSpreadsheet 已改 MIT | ||
firebase/php-jwt | ^6.10 | BSD-3-Clause | ✅ 干净 | ||
laminas/laminas-escaper | ^2.18 | BSD-3-Clause | ✅ | ||
psr/log、psr/simple-cache | ^3.0 | MIT | ✅ | ||
predis/predis(dev) | ^3.0 | MIT | ✅ | ||
phpunit/phpunit(dev) | `^10.5.16 \ | \ | ^11.2` | BSD-3-Clause | ✅ 仅 dev |
friendsofphp/php-cs-fixer(dev) | ^3.47.1 | MIT | ✅ 仅 dev | ||
| Symfony 组件族(22 个,经 php-cs-fixer 传入) | — | 全 MIT | ✅ 仅 dev | ||
| ReactPHP 组件族(7 个) | — | 全 MIT | ✅ 仅 dev |
9.2 管理后台前端(w2r.site/admin-frontend/package.json:11-29,135 个已安装包)
| 许可证 | 包数 |
|---|---|
| MIT | 121 |
| ISC | 9 |
| BSD-3-Clause | 3 |
| BSD-2-Clause | 1 |
| Apache-2.0 | 1 |
非 MIT 的全部清单(无一有传染性):cliui、get-caller-file、picocolors、require-main-filename、set-blocking、which-module、y18n、yaml、yargs-parser(ISC);normalize-wheel-es、source-map-js、speakingurl(BSD-3-Clause);entities(BSD-2-Clause);typescript(Apache-2.0)。
重点:element-plus = MIT(@element-plus/icons-vue 同为 MIT)。这是问题里点名要看的,结论是完全可商用、无需付费、无 copyleft 义务。vuedraggable 及其底层 sortablejs 也是 MIT。
9.3 官网前台(demo.w2r.site/frontend/package.json:11-19,49 个已安装包)
MIT 45、Apache-2.0 1(detect-libc)、BSD-2-Clause 1(entities)、BSD-3-Clause 1(source-map-js)、ISC 1(picocolors)。依赖极薄:只有 vue + vue-router 两个运行时依赖,vite / sass / @vitejs/plugin-vue 三个构建依赖。
全仓 grep -rl -i "GNU General Public\|GNU Affero\|GNU Lesser" --include=package.json 零命中。
9.4 Python 工具链(w2r.site/.venv)
bcrypt Apache-2.0、requests Apache-2.0、certifi MPL-2.0、charset_normalizer MIT、pip MIT;pymysql / idna / urllib3 的 METADATA 无 License 头(实际分别为 MIT / BSD-3-Clause / MIT,以各自 LICENSE 文件为准)。MPL-2.0 是文件级弱 copyleft,只在你修改 certifi 源码本身并分发时才有义务;本项目只是调用,不触发。
9.5 两个不是许可证、但会花钱的邻居
| 项 | 事实 | 影响 |
|---|---|---|
| MySQL 5.7.44 | docs/software-bom.md:19 | Oracle 自 2023-10-25 起将 5.7 移入 Sustaining Support,实际含义是不再发安全补丁(mysql.com EOL 公告,检索 2026-08-11)。这不是许可证费,是 EOL 风险:要么升 8.0 / 迁 MariaDB(人力成本),要么买商业支持 / 用云厂商的付费延长支持。必须排期。 |
| ffmpeg | docs/cms/video-delivery-and-transcoding.md:54(转码 Worker 依赖 ffmpeg) | ffmpeg 默认 LGPL、启用 x264/x265 后为 GPL。但本项目是服务端调用、不随产品分发,copyleft 义务不触发(推断,建议法务复核)。H.264/AVC 的专利池授权是另一条线,需甲方法务确认现有 VW 全球协议是否已覆盖。 |
10. 云资源
10.1 现状(全部为事实)
- 单机自建,不是托管服务:
w2r.site与demo.w2r.site同机(47.242.46.253),宝塔面板 + nginx,MySQL 5.7.44 装在同一台。 - 改写规则写在 Apache 语法的
.htaccess里(w2r.site/.htaccess:1-14、localdev/vhosts.conf复刻的也是 Apache),而 nginx 不读.htaccess—— 真实生产站点配置未交付。这直接影响你能不能在新机器上把站点跑起来。 - DAM 存本地磁盘,没有对象存储:
docs/cms/dam-design.md全篇storage_key都是文件路径(:181 / :246 / :265 / :531 / :554),全仓 grep 无 OSS / S3 / COS 客户端代码或 SDK。上传目录backend/writable/uploads/(docs/project-introduction.md:50)—— 而backend/writable/整个目录不在交付包里(ls w2r.site/backend/无writable)。 - 备份责任明确写成「不归应用管」:
docs/cms/log-inventory-excel-en.tsv:16原文Backup execution success/failure … N/A (out of app scope) … Database/file backup performed by operations (MySQL dump, volume snapshot). CMS does not execute backups。全仓无.sql、无mysqldump脚本、无快照策略。docs/cms/config-promotion.md:354-364那个「备份」只是配置 JSON 的备份,不是数据库备份。 - 视频要双精度:
docs/cms/video-delivery-and-transcoding.md规定在线播放 720p、下载 1080p,同一支片子存两份;docs/cms/requirements.md要求 DAM 支持 2–5 GB 大文件。存储与流量成本的主要来源在这里。
10.2 要买什么(价格均为检索所得,标注为促销价的不可作预算基准)
| 资源 | 价格线索(检索 2026-08-11) | 备注 |
|---|---|---|
| 阿里云 ECS(香港,2 核 4G / 5M 带宽) | 199 元/年(u1 实例活动价) | 活动价,续费按标准价;仅够跑 demo |
| 阿里云 ECS(内地,同档) | 需另行询价 | 要备案就必须是这一类:内地地域 + 包年包月 ≥ 3 个月 + 有公网 IP,才能申请免费备案服务码 |
| 按量付费 ECS | 0.1382 元/小时起 ≈ 101 元/月 | — |
| OSS 标准存储 | 0.09–0.12 元/GB/月(两个来源口径不同) | 低频 0.08、归档 0.015 元/GB/月 |
| OSS 外网流出流量 | 100 GB 下行流量包 294.99 元/年 | 流量是大头,不是存储。视频站尤其如此 |
| CDN | 未取得明确价格 | 大文件视频分发几乎必配 |
| 宝塔面板 | 专业版 549 元/年/台、企业版 999 元/年/台(活动价,原价 699 / 1,399) | 免费版能跑;专业版起才有网站防火墙、防篡改、系统加固——这几项对「等保二级导向」直接相关 |
| 数据库备份 | 自建 mysqldump(0 元 + 人力)或迁 RDS(另计) | 当前状态是「没有任何人证明备份存在」 |
| WAF | 未取得明确价格 | docs/cms/requirements.md:82 把 WAF 列入 staging 环境要求 |
11. 采购清单(汇总)
| 项目 | 是否强制 | 谁持有 / 谁买 | 大致费用区间 | 时效性说明 |
|---|---|---|---|---|
| ICP 备案(新增网站) | 法规强制(仅当服务器在中国内地) | 甲方主体(京ICP备08103718 已在手),甲方 IT 提交 | 备案本身 0 元,成本转嫁到内地 ECS | 服务器在香港则不需要也办不了。上线形态一变就要重办 |
| 公安联网备案 | 法规强制(ICP 备案后 30 日内) | 甲方 IT | 0 元 | 以 ICP 备案号为前置;境外服务器无法办理 |
| 页脚备案号写法 + 链接 + 警徽 | 法规强制 | 接手团队改 SiteFooter.vue:51-52 | 0 元(改代码) | 上线前必须 |
| 内地 ECS(为备案) | 条件强制 | 甲方 IT | 香港档 199 元/年起(促销);内地档需询价 | 必须包年包月 ≥ 3 个月 + 公网 IP |
| 域名 w2r.site 续费 | 视是否保留 | 归属未确认 | 几十至两三百元/年(.com 参考 95 元/年) | 2027-02-08 到期,不足半年 |
| 域名 digirepub.com | 不建议依赖 | 乙方(上海大合数码) | 不适用 | 2027-02-04 到期;甲方无控制权 |
| SSL 证书(OV,建议含通配符) | 事实强制(全站 HTTPS 是 checklist-implementation.md:13/25/26 的自订要求) | 甲方 IT 买、运维续 | DV 免费(90 天)→ DV 通配符 880 元/年 → OV 通配符 3,980 元/年;全线 238–11,250 元/年 | 当前疑似 90 天 ACME 自动续期,续期责任人未知 |
| 等保二级测评 | 视定级结论(若定二级则强制) | 甲方安全 + 第三方测评机构 | 测评报告 3–8 万元/次;含整改与设备总投入 20–55 万元 | 建议每 2 年 1 次 |
| 等保三级测评(若升级) | 强制(升三级后) | 同上 | 报告 8–20 万元/次;总投入 51–178 万元 | 每年 1 次 |
| 密评 | 当前不触发 | — | 若触发:5–15 万元/次(行业经验值,非检索所得) | 仅在认定为 CII 或升等保三级时强制 |
| The Group 字体 webfont 使用权 | 品牌强制(非法规) | Volkswagen AG / VW 品牌部门持有 | 通常不单独售卖,走品牌门户授权 | 必须书面确认;同时应改 WOFF2 + 子集化 |
| Monotype 翔鹤黑商用授权 | 可能已侵权 | 需查明谁买的 | 未取得公开报价,需联系蒙纳或代理 | 4 个字重已在 backend/public/ 公网可下载 |
| GroupUI CSS 使用权 | 品牌强制 | VW SDC 持有 | — | 已 vendored 进公开目录,需确权 |
| 中文字体(替代方案) | 强烈建议 | 接手团队 | 0 元(思源黑体 / Noto Sans CJK SC,SIL OFL 1.1,明确允许 webfont 商用) | tokens.scss:19-21 当前无任何 CJK 字体 |
| Cursor Teams 席位 | 非强制 | 接手团队 | $32/席/月(年付)、$40(月付);Premium $96/$120 | 先索要缺失的 15 条 rules + 10 个 skills,再决定买不买 |
| Figma 席位 | 短期强制(前台 11 张图靠它活着) | 接手团队 + 设计 | Full $16/席/月(年付)、$20(月付);Dev $12/$15 | 把 11 条外链落盘后可降级为纯设计工具 |
| Figma 源文件所有权 | 强制 | 甲方 | 0 元(交付物) | Homepage-Demo1 文件必须转交或完整导出 |
| 对象存储 OSS(若 DAM 迁走) | 非强制 | 甲方 IT | 0.09–0.12 元/GB/月;流量包 100 GB / 294.99 元/年 | 视频 720p+1080p 双份,流量是主要成本 |
| CDN | 建议 | 甲方 IT | 未取得明确价格 | 2–5 GB 大文件分发几乎必配 |
| 数据库备份 | 事实强制 | 甲方 IT / 运维 | 自建 0 元 + 人力,或迁 RDS 另计 | 当前无任何备份证据;全库无 .sql |
| 宝塔面板授权 | 非强制 | 甲方 IT | 专业版 549 元/年/台、企业版 999 元/年/台(促销价) | 免费版可跑;防火墙 / 防篡改是付费项,与等保相关 |
| MySQL 5.7 升级或商业支持 | 事实强制 | 接手团队 / 甲方 IT | 升级=人力;商业支持另计 | 5.7 自 2023-10-25 起不再发安全补丁 |
12. 三个必须先定、否则整张单子作废的决策
- 生产服务器落在内地还是境外? 内地 → 备案链条全套启动、内地 ECS 必买、页脚号要新增;境外 → 备案不做也做不了,那页脚那行必须删掉或改写,不能留着一个不属于本域名的备案号。
- 正式域名用哪个? 用乙方的
vgc.digirepub.com等于把生产入口交给代理商;用甲方自有的volkswagengroupchina.com.cn才是可持续形态,但那要走替换现网 Sitecore 的完整切换。 - 等保定几级? 二级和三级之间差着一年一次的强制测评、差着密评是否触发、差着 30–100 万的安全设备预算。这个结论不能由开发方给。
还需要人来决定的 51 个问题
- 139.224.64.38 / vgc.digirepub.com 这台机器现在跑的是哪个版本的代码?docs/project-introduction.md:44 把它标为「生产域名」,但两个 zip 都来自 47.242.46.253,这台机器零交付。它是真生产、是废弃环境,还是文档写错了?
- 交付方是否还持有 backend/writable/ 的备份?如果 DAM 原始文件(uploads/dam/)已经在服务器上被清理或从未备份,那么 assets 表里的记录将全部指向不存在的文件——这一项决定了迁移工作量是「拷贝」还是「重做」。
- demo.w2r.site 从 2026-03-23 到 2026-06-21 的三个月增量代码在哪里?既然该站点无 Git、无 CI,这批代码是只存在于服务器磁盘上,还是交付方本地另有副本?如果只在服务器上,必须立刻取回。
- docs/changelog.md 停在 2026-06-21,但源码文件时间戳到 2026-07-22。这一个月里到底改了什么?是真有代码变更还是只是打包时的批量拷贝导致时间戳统一?(我看到 07-20 17:51 与 07-22 09:30 两个时间成批出现在包括 3 月旧迁移在内的大量文件上,倾向后者,但无法确认。)
- 「等保二级」目前处于什么阶段——已完成测评、正在测评、还是仅在设计上对齐?有没有测评机构、报告编号或备案回执?这决定了上线前是否需要补测评周期。
- backend/public/AdM/ 里的构建产物是否由本次交付的 admin-frontend/js/ 源码构建而来?没有 CI、没有产物 hash,唯一的验证办法是接手方自己重新构建后比对——请交付方确认最后一次 npm run build 的时间与对应的源码状态。
- backend/.env 里的 Sitecore 账号(第 16–19 行)归属于谁?旧官网 211.151.111.91 下线后这套凭据是否失效?如果失效,未完成的历史内容迁移就永久断了。
- GitLab 上三个仓库里,是否存在本次交付之外的分支、tag 或 MR?(backend 本地只看到 main 与 origin/main。)如果有 release tag,那才是真正该对齐的基线。
- w2r.site 域名 2027-02-08 到期,注册账号在谁手里?续费与后续的 DNS 变更由哪一方执行?
- TheGroupHEAD / TheGroupTEXT 两款品牌字体的 Web 分发授权是否已获得?授权覆盖到哪些域名、有无并发或流量上限?
- test 与 staging 环境(requirements.md:78-90 强制要求)是从未搭建过,还是搭过后已经拆掉?若从未有过,「上 prod 前在 staging 跑验证清单」这条流程实际上从来没执行过——需要甲方确认是否接受。
- docs/checklist.xlsx 那 53 行检查表由哪一方出具?现在的原件在谁手里?checklist-implementation.md 里已有 20 多项标为 N/A 或「部分实现」,这些判定甲方是否已经书面认可?
- 生产域名 vgc.digirepub.com(139.224.64.38)现在归谁运维?上面跑的是这套 CMS 吗、还是别的东西?它与 47.242.46.253 上的 w2r.site / demo.w2r.site 是什么关系——同步、替代、还是各自独立?(docs/project-introduction.md:44 把它标为「生产域名」,但仓库里没有任何指向它的代码、配置或凭据)
- 47.242.46.253 上两个站点各自绑定的是哪个 PHP 版本?demo.w2r.site/backend/README.md:7 说这台机器 putenv 被禁用、mbstring 未安装,但 w2r.site/backend/composer.json:13-17 要求 mbstring/intl/mysqli/imagick 齐全——两站是不是跑在不同的 php-fpm 池上?每个池的 disable_functions 具体是什么?
- 线上 nginx 的两份站点配置能不能拿到原文?特别是 demo.w2r.site 的 /api 是怎么转发的——转给 api.php 还是 index.php?以及 /page-css/ 是不是做了静态映射?(这决定 §5 的两套并行 API 实现里哪一套是真在跑的)
- 三条 spark 命令(publish:run-schedule / audit:cleanup / logs:cleanup)在线上到底配没配 cron?如果配了,crontab 原文是什么、用的哪个 PHP 二进制?如果从来没配,那定时发布/下线功能是不是自上线起就没生效过?
- w2r.site/backend/writable/ 现在有多大?里面有多少个 DAM 资产文件?(这是容量规划、迁移窗口和备份策略的唯一依据,而整个目录未交付)
- demo.w2r.site 的 open_basedir 与 shared/dam-writable 符号链接的冲突在线上是怎么解决的?是宝塔站点配置或 php-fpm pool 里额外放行了 /www/wwwroot/w2r.site/,还是线上的 .user.ini 内容与交付的这份不同?
- w2r.site 这个域名 2027-02-08 到期。切到 vgc.digirepub.com 的时间表是什么?切换后 demo.w2r.site 这个前台域名怎么处理——继续保留作测试,还是一并废弃?(决定 contentPreview.js:2 的 PUBLIC_SITE_ORIGIN 该改成什么)
- 官网页脚硬编码的备案号(京ICP备08103718-21、京公网安备 11010502040869号,见 demo.w2r.site/frontend/src/components/home/SiteFooter.vue:51-52)对应的是哪个主体、哪个域名?新域名上线前需要重新备案或变更接入商吗?
- GitLab(gitlab.onedevops.vw.com.cn/c-sdxx/corporate-website/)的三个仓库 backend / frontend / corporate-website,接手团队能拿到访问权限吗?demo.w2r.site 从来不在版本控制里,它的历史变更还能追溯吗?
- database 全库导出能不能拿到?如果拿不到,是接受用 51 个迁移 + RolePermissionSeeder 重建空库、业务数据重新录入,还是另有导出渠道?
- 视频转码在一期验收范围内吗?docs/cms/video-delivery-and-transcoding.md:54 说依赖 ffmpeg 与异步 Worker,但 app/Commands/ 下没有任何转码命令——是没做,还是做在别处?
- 现有 Sitecore 系统(211.151.111.91 / cm.volkswagengroupchina.com.cn)的下线时间点是什么?w2r.site/backend/.env:16-18 存的那套凭据现在还需要保留吗?
- 这个项目要不要真的过等保二级测评?文档写的是「等保二级导向」而不是「已通过」。如果要测评,多环境隔离、生产不开 debug、密钥不明文这几条现在都不满足,需要在时间表里预留整改窗口。
- 等保定级结论到底是二级还是三级?文档只写「等保二级导向」,仓库中无定级报告。这必须由甲方信息安全 / 法务与属地公安网安支队确认,交付方无权单方面认定。二级与三级在测评频率、是否强制密评、安全设备清单上差距是数量级的
- 定级对象怎么划?47.242.46.253(同机跑 w2r.site 与 demo.w2r.site)、139.224.64.38(vgc.digirepub.com,docs/project-introduction.md:44 标为「生产域名」但无任何代码交付)、211.151.111.91(现有 Sitecore 官网)三台服务器分别算不算定级对象、算几个定级对象?边界不清就无法编定级报告
- 真正的生产环境是哪一台?交付代码来自 47.242.46.253,但文档把 139.224.64.38 标为生产域名。等保测评是对生产系统测,测错机器整个流程作废
- 这套 CMS 与官网将来是否要替代 211.151.111.91 上的现有 Sitecore 官网?如果是,切换本身构成「系统重大变更」,即使 Sitecore 侧已有等保备案与测评报告,也不能沿用,须重新测评
- 现有的 volkswagengroupchina.com.cn 官网(Sitecore)此前是否已经做过等保定级备案?定的几级?测评报告还在有效期内吗?如果有,新系统的定级应与之衔接,不能凭空另起
- 甲方内部谁是等保责任人 / 网络安全负责人?《网络安全法》第二十三条要求「确定网络安全负责人,落实网络安全保护责任」。目前没有任何一方被指定,这是整件事推不动的根本卡点
- 生产环境的真实站点配置(nginx server block、SSL 配置、是否 301 到 HTTPS、是否有 WAF 在前)是什么?仓库里只有 Apache 语法的 .htaccess,nginx 不读它;应用层 forceGlobalSecureRequests 是 false,无法判断线上传输加密的真实状态
- backend/app/Commands/CleanAuditLogs.php 的 audit:cleanup 命令在生产上到底有没有配 crontab 在跑?仓库中无任何定时任务配置交付。如果没跑,「审计日志保留 6 个月」这条只是文档上的说法
- 当前生产 .env 里的 JWT_SECRET 是不是也是交付包里那个开发占位串?如果是,属于需要立即处置的在线风险,不能等排期
- 数据库和 DAM 媒体文件(backend/writable/,整个不在交付包里)现在有没有任何备份?备份在哪、多久一次、有没有做过恢复演练?等保二级即要求数据备份恢复,目前零证据
- 甲方对整改预算的接受区间是多少?测评费之外,安全整改与设备采购是变数最大的一块(市场口径 5 万-30 万元以上),这个数字直接决定定二级还是三级的决策
- 官网前台后续是否会增加表单类功能(订阅、留言、招聘投递、试驾预约)?当前 demo.w2r.site/frontend/src 全量检索无任何个人信息采集入口,这是支撑「可以定二级」的关键论据;一旦加了表单开始收集公众个人信息,定级依据就要重估,且触发《个人信息保护法》另一套义务
- 生产环境最终部署在哪里?交付的 47.242.46.253 是香港节点(境外),文档标注的生产域名 vgc.digirepub.com 解析到 139.224.64.38(上海,内地)却没有交付任何代码。这个项目到底上线了没有、上线在哪台、跑的是哪份代码?代码里查不到答案。
- w2r.site 这个域名在谁的阿里云账号下?续费谁付?合同结束后过户给甲方吗?仓库里只有到期日(2027-02-08),没有账号归属。
- vgc.digirepub.com 是要长期用作生产域名,还是只是乙方的临时演示地址?digirepub.com 属于上海大合数码科技有限公司(DIGIREPUB 数字共和),甲方是否知情并接受生产入口在代理商域名下?
- 生产 SSL 证书是谁签发的、有效期多久、谁在续期、续期是自动的还是手动的?仓库里没有任何证书文件,只有一个 ACME 验证残留文件。如果是 90 天自动续期,续期的账号和 cron 在谁手里?
- TheGroupHEAD-Light.ttf 与 TheGroupTEXT-Regular.ttf 这两个文件是从哪里来的?有没有 VW Group 品牌部门或 SDC 的书面下发记录?品牌规范允许在公网站点以 webfont 形式投放吗?
- Monotype 蒙纳「翔鹤黑」(MXiangHeHeiSCStd 四个字重)的商用授权是谁买的?授权覆盖 webfont 投放吗?覆盖大众这个使用主体吗?还是说这四个文件只是开发时随手放进去的?
- 中文字体最终用哪一款?CMS 那侧的 groupui-showcase.html 已经选了翔鹤黑,但官网前台 tokens.scss:19-21 一个 CJK 字体都没有。这是设计未定,还是授权没谈下来所以没敢用?
- 等保最终定几级?谁出定级报告?已经做过测评了吗?文档只写「等保二级导向」,全仓找不到定级报告或测评报告。
- 这个系统会不会被认定为关键信息基础设施?这决定密评是不是强制年度项。按企业官网的常规判断不会,但结论要甲方安全出。
- backend/writable/ 整个目录(DAM 媒体 + 运行时)和全库数据库导出在哪里?没有这两样,备份策略、存储容量测算、对象存储迁移方案全都是估的。
- 真实的 nginx 站点配置在哪里?仓库里的改写规则全是 Apache 的 .htaccess 语法,nginx 根本不读。生产上是谁写的 nginx conf、放在哪?
- 缺失的 15 条 .cursor rules 与 10 个 skills 能不能拿到?如果拿不到,接手团队需要自己重建这套开发约束,Cursor 席位的采购必要性也要重新评估。
- Figma 文件 Homepage-Demo1 的所有权归谁?能转到甲方的 Figma 组织下吗?backend/.env 里那个 figma.token 是谁的个人 token,注销后 CMS 的 Figma 导入功能会不会直接失效?
- 甲方现有的 IT 采购框架里,阿里云、SSL 证书、Figma、Cursor 这几项有没有已经存在的集团级协议或统一采购通道?如果有,单独去买反而更贵也更难报销。
其余 37 条行动项
接手后 30 天内 取回 /www/wwwroot/w2r.site/.cursor/(15 条 rules + 10 个 skills)与 /www/wwwroot/.cursor/rules/(workspace 级,含 005-demo-media-permissions-mfa.mdc 与 URL 语言约定)
docs/project-introduction.md:266-299 有完整清单,压缩包里 .cursor 条目数为 0。分层约定、错误码分段、审计规范、媒体权限完整规则全部只剩标题
接手后 30 天内 取回 workspace 级 docs/ 目录:changelog.md、demo-video-media-delivery.md、demo-media-permissions-mfa.md、figma-designer-checklist.md
w2r.site/docs 内有 6 处 ../../../docs/ 或「见 workspace」的悬空引用(changelog.md:87/168/237/266、video-delivery-and-transcoding.md:4、figma-import.md:5),关键实现说明只剩指针
接手后 30 天内 补交 6 个缺失脚本:import_vgc_news.py、import_mediacenter_news.py、md_to_docx.py、gen_interface_docs.py、stats_sitecore_files_import.py、migrate_config_v1_to_v2.php,并附一份覆盖全部脚本的 requirements.txt
scripts/README.md:181/252/293/315、docs/README.md:8、docs/import_sitecore_files.md:86-92、docs/cms/config-promotion.md:193 分别指名这些脚本。此外 .venv-doc 与 .venv-
接手后 30 天内 把 docs/ 与 scripts/ 从 .gitignore 移出并纳入版本控制(w2r.site/.gitignore:5 和 :7),同时为 demo.w2r.site 建立 Git 仓库并做首次提交
31 份文档、12 个迁移脚本、整个官网前台目前只存在于这份 zip 和服务器磁盘上,任何一处丢失即彻底丢失
接手后 30 天内 为认证/MFA/RBAC/工作流/发布/预览/DAM 分片/搜索/审计九个模块补最小回归测试,并在 .gitlab-ci.yml 里加 build 与 test stage
backend/tests/ 只有 5 个 Figma 测试 + 1 个 Health 测试 + 4 个 CI4 样板;两个前端 package.json 都没有 test script;CI 只有 SAST 与 Secret Detection,无构建流水线,导致 public/AdM/ 构建产物
接手后 30 天内 落实 TheGroupHEAD-Light.ttf 与 TheGroupTEXT-Regular.ttf 的 Web 分发授权,取得书面 EULA 归档
两个字体文件随 demo.w2r.site/frontend/src/assets/fonts/ 分发并已打进 dist/assets/,交付物中无任何授权文件
接手后 30 天内 补一份最小可用的部署运维手册:环境要求、初始化步骤(migrate + seed + CreateSuperAdmin)、构建命令、cron 清单、备份与恢复、回滚流程
w2r.site/README.md 是 GitLab 默认模板原文,backend/README.md 是 CodeIgniter 官方发行版 README 原文,交付物中没有任何 runbook
可排期 用 backend/app/Config/Routes.php 反向生成 OpenAPI 规范,与 docs/Interface-List.docx、Interface-Agreement.docx 对账
接口文档只有两个二进制 Word 且无 Markdown 源稿,生成脚本 gen_interface_docs.py 也缺失。没有机器可读规范就无法做契约测试,Word 与代码一旦不符没人会发现
可排期 决定 VisualEditor.vue 的去留:要么补路由接入并完成断点配置与 ?device= 预览,要么明确降级并同步修订 requirements.md 与 acceptance-checklist.md
该文件在全树零引用(grep 无命中),是死代码;而 project-introduction.md:30 把「可视化拖拽」列为核心目标,acceptance-checklist.md:73-76 有 4 条相关验收项
可排期 补建审核工作台、搜索热词/置顶管理、FAQ 主题管理三个后台页面
后端接口齐备(Routes.php:117-120、:163-171、:107-111)但前端无入口,导致 acceptance-checklist.md 中工作流、搜索、FAQ 三组验收项无法在 UI 上演示
接手后 30 天内 重写 docs/cms/dev-plan.md,或在文首加显著声明标注其已过期
该文档 :107/:132/:164/:197/:229 把工作流、发布预览、DAM、搜索、系统配置五个阶段全标为「待实现」,:315 写「已完成阶段 1-3」,但这些接口在 Routes.php 里全部存在。按此文档判断进度会严重误判
接手后 30 天内 用 localdev/ 里那套(Dockerfile + vhosts.conf + docker-compose.yml)为基础搭建独立 staging,并给测试站加 Disallow: / 与 IP 白名单/Basic 认证
现在没有任何独立测试或预发环境;两站 robots.txt 均为全站放行,测试内容会被搜索引擎收录并与正式站争排名。localdev 已被验证能把两站跑通,是现成起点。
接手后 30 天内 决定 demo.w2r.site 的 /api 两套并行实现留哪一套(public/api.php 裸 PHP 与 CI4 Controllers/Api.php、News.php 功能重叠)
api.php:12-52 与 app/Config/Routes.php:12-23 注册了同一批端点;哪一套生效完全取决于未交付的 nginx 配置。保留两套意味着改一处不改另一处会产生行为漂移,且 api.php:19 的 CORS 是 *。
接手后 30 天内 补齐 config-promotion.md 承诺但代码里没有的部分,或反过来修订文档:版本兼容校验、真实的 dry_run 校验、导入前备份到 writable/backups/config/、POST /api/config/restore、export-all/import-all
ConfigPromotionService.php:72-83 的 dry_run 不做任何校验就返回 validated=true;version 参数只回显不校验;Routes.php 中无 restore 路由;:209 的 channel_auth 导入是覆盖式 replaceForCha
接手后 30 天内 把 demo.w2r.site 纳入版本控制,并把 .htaccess / .user.ini / nginx conf / crontab 从 .gitignore 里放出来单独归档
find 确认 demo.w2r.site 整个目录没有 .git;w2r.site/.gitignore:5,7,11,14,15 把 docs/、scripts/、writable/、.htaccess、.user.ini 全部排除。结果是没有任何一份环境配置是可 diff、可回滚的。
接手后 30 天内 确认 publish:run-schedule 在无到期任务时是否也写审计,若是则改为只在有变更时写
RunPublishSchedule.php:22-40 每次执行都调 AuditLogger::log 写一条 publish_schedule_run。按每分钟一次算,每月约 43,200 条系统审计会淹没 operation_logs,而 CleanAuditLogs.php:25 的保留期是
可排期 为生产环境补 CI/CD 部署管线(当前 .gitlab-ci.yml 只有 SAST 与 Secret-Detection)
w2r.site/.gitlab-ci.yml 第 12-13、21-22 行只 include 了两个安全扫描 template,stages 只有 test 与 secret-detection,没有 build、deploy 或 environment 定义。部署完全靠手工,无发布记录、无回滚
可排期 确认视频转码是否在一期范围内;若在,补齐转码 Worker 与 ffmpeg 部署
docs/cms/video-delivery-and-transcoding.md:54 写明依赖 ffmpeg 与异步 Worker,但 app/Commands/ 下只有 DeleteVideoMp4Assets.php,没有任何转码命令。服务器要不要装 ffmpeg 取决于这个答案。
接手后 30 天内 确定等保定级结论(二级还是三级),编制定级报告并组织 3-5 名专家评审
定级是全部等保工作的起点,法定主体是运营使用单位而非交付方。二级与三级在测评频率(两年 vs 每年)、是否要做密评、安全设备清单上差距是数量级的
接手后 30 天内 界定定级对象边界:47.242.46.253(阿里云,同机跑 w2r.site 与 demo.w2r.site)、139.224.64.38(vgc.digirepub.com,project-introduction.md:44 标为生产域名但无代码交付)、211.151.111.91(现有 Sitecore 官网)三者各自算不算定级对象、算几个
定级对象边界不清就没法编定级报告,测评机构也无法确定测评范围。这是定级的前置条件
接手后 30 天内 确认 backend/app/Commands/CleanAuditLogs.php 的 audit:cleanup 是否真的配了 crontab 在跑,并把保留期由默认 6 个月改为 12 个月
仓库中无任何 crontab 配置交付,无法确认清理任务是否在执行;且现在的逻辑是「删掉超过 6 个月的」,正好卡在《网络安全法》第二十三条法定下限上,没有缓冲
接手后 30 天内 建立数据库与 DAM 媒体的备份策略并做一次恢复演练,留存演练记录
仓库中无备份脚本、无备份策略文档、无恢复演练记录,scripts/ 下 13 个 Python 文件全是 Sitecore 迁移脚本;交付包中也无任何 .sql 全库导出。等保二级即要求本地备份恢复,三级要求异地
接手后 30 天内 审计日志防篡改落地:应用 DB 账号剥离 operation_logs 表的 UPDATE/DELETE 权限,清理任务改用独立账号
logging-system-design.md:199-203 把这条写成「实现建议」但未实施。当前应用与清理任务共用同一个 DB 账号,审计表在应用权限内可删可改,等保安全审计「审计记录保护」判不符合
接手后 30 天内 交付真实的生产 nginx 站点配置、防火墙策略与服务器 NTP 时钟同步配置
生产疑似 nginx(404.html 是 nginx 默认页),但改写规则写在 Apache 语法的 .htaccess 里,nginx 不读。测评第一天就会索要这些配置;时钟同步是审计追溯的前提,必查
可排期 编写安全管理制度体系文件(三级约 30 余项,二级可精简),优先级最高的是:账号与权限管理制度、安全审计管理制度、数据安全与备份恢复管理制度、网络安全应急预案、个人信息保护管理制度、软件开发安全管理办法、第三方服务商安全管理制度
仓库中一份管理制度都没有。管理类要求在测评打分中权重不低,且要求「有制度、有执行、有记录、可核查」的完整证据闭环,写出来不等于做到
可排期 选择公安部认可资质的测评机构,先做差距分析再进整改,不要跳过差距分析直接约测评
当前状态下直接测评必然不符合,白花一次费用。差距分析报告是整改方案与采购清单的依据
可排期 采购并上线安全设备:二级需防火墙、IPS/IDS、主机防病毒、日志审计(≥6 个月)、漏扫、SSL 证书;若定三级还需 WAF、堡垒机、数据库审计、准入控制、上网行为管理、集中安全管理/态势感知、核心设备冗余、异地备份
当前是宝塔面板 + 单机部署形态(.user.ini 显示 open_basedir=/www/wwwroot/w2r.site/),离等保要求的架构差距很大。云上部署可用云厂商打包产品降低成本
可排期 补做一次真正的漏洞扫描与渗透测试
现有 security-scan-results/security-scan-report-20260125_011114.json 是依赖成分分析(SCA),npm 与 composer 各 0 漏洞,但这不是漏扫也不是渗透测试,测评不认这份报告
可排期 若最终定为三级,另行安排商用密码应用安全性评估(密评),并在系统投入运行前完成
网络安全等级保护第三级及以上信息系统属密评对象,须通过密评方可投入运行,运行后每年至少评估一次并报市级密码管理部门备案。这是三级相比二级的额外年度成本
可排期 把交接文档中所有引用《网络安全法》第二十一条 / 第五十九条的表述更新为第二十三条 / 第六十一条
《网络安全法》2025 年 10 月 28 日修正、2026 年 1 月 1 日施行,条款已重新编号,罚款上限由 50 万提至 100 万,严重情形 50 万-200 万。引用旧条号会被认为对现行法规不熟
接手后 30 天内 由甲方安全 / 法务出具等保定级结论(二级还是三级),并据此启动测评机构招标。
docs/project-introduction.md:25 与 docs/cms/requirements.md:162 写的是「等保二级导向」,导向不等于已测评,全仓无定级报告与测评报告。二级与三级之间差着测评频次(2 年 1 次 vs 每年 1 次)、差着密评是否强制触发、差着 30–100
接手后 30 天内 决定 w2r.site 域名是否续费并明确控制权归属,若保留则在 2027-02-08 前完成续费与过户。
该域名 2026-02-08 才由阿里云万网注册、2027-02-08 到期(localdev/report/shell.html:312),距今不足半年,且 LOG.md:88 与 Progress.md:18 都记为归属未确认。续费金额只有几十到两三百元,风险在控制权不在钱。
接手后 30 天内 把 fonts.scss:5/13/22 与 groupui-showcase.html:16-48 的字体投放从 format('truetype') 改为 WOFF2 + 字符子集化,并加 referer 限制。
当前两套受保护字体都以原始 TTF 明文投放,任何访客都能下载到可直接安装到本机的字体文件。翔鹤黑 4 个字重合计约 10 MB,转 WOFF2 加子集后还能顺带把首屏体积砍掉一大截。
可排期 排期 MySQL 5.7.44 升级到 8.0(或迁 MariaDB),或采购商业延长支持。
docs/software-bom.md:19 记录生产库为 MySQL 5.7.44。Oracle 自 2023-10-25 起将 5.7 移入 Sustaining Support,实际含义是不再发安全补丁。这是安全敞口,不是许可证费,但和等保测评直接冲突。
可排期 评估是否采购宝塔面板专业版(549 元/年/台)或企业版(999 元/年/台,促销价)。
免费版能跑,但网站防火墙、防篡改、系统加固这三项是付费档才有的功能,和「等保二级导向」的整改项直接相关。买不买取决于最终等保定级和是否另行采购独立 WAF。
可排期 若 DAM 媒体要从本地磁盘迁到对象存储,按 OSS 标准存储 0.09–0.12 元/GB/月 + 流量包(100 GB 下行 294.99 元/年)测算,并把 CDN 一并纳入。
docs/cms/dam-design.md 全篇 storage_key 都是本地文件路径,全仓无 OSS/S3/COS SDK。而 video-delivery-and-transcoding.md 要求 720p 与 1080p 双份存储、requirements.md 要求支持 2–5 GB
可排期 由法务复核 ffmpeg 转码链路的 H.264/AVC 专利授权是否已被 VW 全球既有协议覆盖。
docs/cms/video-delivery-and-transcoding.md:54 规定转码 Worker 依赖 ffmpeg。ffmpeg 本身因为是服务端调用、不随产品分发,LGPL/GPL 传染不触发(此为推断,需复核);但 AVC 专利池授权是另一条独立的线。
