示意图

技术尽调报告 · 2026-08-12

VW 官网 CMS
收到了什么,还差什么

对方交付了两个压缩包。本报告逐项清点:里面有什么、成色如何、离上线还差多少、哪些我们能自己解决。

约 90% 由 AI 写成

01 · 交付清点

收到的全部东西:两个压缩包

w2r.site.zipCMS 后台系统241,815,795 字节
demo.w2r.site.zip官网前台101,044,255 字节
交接文档
部署说明
数据库导出
图片视频文件

成色:后端是新的,官网是旧的

各部分最后一次改动对方变更记录到 06-21CMS 后端2026-07-22CMS 管理后台2026-07-22迁移脚本2026-07-22官网前端2026-06-12官网后端2026-03-23
官网部分明显旧于后端,且旧于对方自己的变更记录

能证明交付的不是最新版

对方自己的变更记录写到 6 月 21 日⁠,里面明确还在改官网首页文案和标题。但交付的官网前端源码停在 6 月 12 日⁠,官网后端更是停在 3 月 23 日⁠。

更直接的两条:变更记录里写着有「首页正文接口」和「外部预览页」,在交付的代码里搜索结果都是 0 处⁠。而后台那一侧的预览功能是完整实现的——说明后端做好了,前台承接页面没给。

02 · AI 撰写占比

这套代码,约九成由 AI 写成

≈90%AI 生成占比

下面全是可复核的实测计数,不含推测。

缩进方式213 个文件全部纯空格,零 Tab100%
花括号风格独占一行 572 处 vs 同行 3 处99.5%
字符串引号单引号 26,918 vs 双引号 59397.8%
TODO / 待办标记27,252 行 PHP 里0 个
代码格式化工具配置不存在

三个人手写 213 个文件、27,000 行、跨越数月,做到这种零漂移在实践中不可能;而且没有任何格式化工具配置,这种一致性不是工具做的。

……需要把现有分类都挂到网站素材下吗?用户说『把栏目的构架复制到资产分类的网站素材下面』……所以保持⁠:只插入根节点,不移动旧数据。

来源:数据库迁移文件 2026-03-04-100031 的注释。

项目规格书第四章列明了用 AI 助手(Cursor)开发,配 15 条规则 + 10 个技能;界面素材经 Figma 导出。

问题不在于用了 AI,在于没有验证环节

自动安全扫描的配置是坏的(三个仓库一模一样,说明同一次错误复制),核心功能测试覆盖为零——代码写出来之后,没有任何机制去检查它对不对,145 个问题就是这么累积下来的。

03 · 安全问题

83 项安全相关问题,11 项属于最严重级别

缺了几块面板的防护墙

下面每一条都标注了技术名称和大白话说明。这些在国家的等级保护检查里会被直接判不合格。

等保(网络安全等级保护)

国家规定:在中国境内运营网站要按重要程度分等级、达到对应安全标准、请有资质机构测评、去公安局备案。没通过不能合法上线。本项目文档写的是「等保二级导向」——「导向」的意思是设计时参考了标准,但既没做过测评也没备案。

145811已修复 改完并验证能修复 同类方法已验证需甲方定 产品或流程决策
83 项安全相关问题的处理状态

CSRF 防护被整段注释掉

已修复别人能冒充你在后台操作

什么意思网站原本有一道防护,专门挡「你在别的网页上被诱导点了一下,结果你的后台账号替攻击者干了活」。这道防护的开关在代码里被注释掉了。
后果我们实测演示过:不用密码,成功让系统签发了一张高权限凭证,还真建了一条数据。
已修复并验证,17 项功能回归全部通过
详情
缺陷记录

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 小时

什么意思系统在数据库里记了「这个账号已退出」,但每次检查身份时根本没去读这条记录⁠。所以退出、被管理员踢下线、账号被停用,三种操作全都不生效。
后果员工离职当天停用账号,他手上的登录凭证在 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 编码

已修复手机验证码的钥匙,等于明文存在数据库

什么意思开了双重验证后,每个人有一把「种子钥匙」存在数据库。代码注释写的是「加密存储」,实际只做了一层编码——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

栏目管理六个接口零权限校验

需甲方定任何登录的人都能改网站导航

什么意思栏目的新增、改名、删除、排序共 6 个功能,完全没有权限检查⁠。只要能登录,哪怕是只读账号,都能拖动整棵导航树。
后果栏目名字是网址的组成部分,改了会导致线上链接失效、搜索引擎收录出错。
已加校验并做成可配置。「该给哪个角色开」需甲方定
详情
缺陷记录

栏目管理全部写接口没有任何权限校验,任意登录角色可增删改栏目

影响

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

内容管理五个写接口零权限校验

已修复任何登录的人都能建、改、删内容

什么意思创建、修改、删除、存草稿、删草稿五个接口只检查「登录了没」,不检查「有没有这个权限」。系统里定义了 `content:create`/`content:edit`/`content:delete` 三个权限,但这条主流程上一个都没用⁠。
后果只读审计账号也能发布和删除文章。
已修复。补了权限校验并逐角色实测
详情
缺陷记录

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

已修复伪装成图片的文件能通过上传检查

什么意思判断上传文件类型时,扩展名和文件真实类型只要中一个就放行(写成了「或」,本该是「且」)。
后果把可执行文件改名成 .jpg 就能绕过检查。
已修复。改为两项都必须匹配

MFA 验证接口无次数限制

已修复6 位动态码可以无限次猜

什么意思输错动态码没有任何次数限制,可以不停地试。6 位数字只有 100 万种组合,自动化脚本很快能试完。
后果双重验证这道防线可被暴力破解绕过。
已修复。接入了失败次数限制
详情
缺陷记录

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 可从网址参数传入

已修复登录凭证会被写进服务器日志

什么意思登录凭证除了正常的请求头,还允许通过网址参数传递。网址会被记进服务器访问日志、浏览器历史,也会通过 Referer 泄露给第三方网站。
后果运维人员翻日志就能捡到别人的登录凭证。
已修复。只保留请求头方式
详情
缺陷记录

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 的自动安全扫描配置写坏了

能修代码从没被安全扫描检查过

什么意思VW 内部开发平台要求代码提交后必须先过自动安全扫描(SAST)。这份配置文件里混进了三个格式错误的配置项,而且关键配置重复定义了两次——三个仓库一模一样⁠,说明是同一次错误操作复制出去的。这种配置在平台上要么直接报错,要么静默忽略。
后果27,000 行后端代码里的 17 个阻断级问题,一次都没被扫到。而本地那份唯一的扫描报告,扫的是另一个目录,PHP 依赖数报 0(实际 17 个)。
配置修复是分钟级工作,但要连同 17 个阻断项一起清掉才有意义
详情
缺陷记录

核心功能代码从未提交,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 个测试,覆盖的是一个边缘功能。核心的登录、权限、审核流程、发布、媒体管理一个测试都没有⁠。
后果这是所有其他问题的根源——没有验证手段,问题就会持续累积而无人发现。
本轮已用「先建验证条件、再改、再回归」的方式实测 14 项
详情
缺陷记录

测试只覆盖 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

审计验收命令会伪造管理员身份

能修有个命令能凭空造出超级管理员权限

什么意思代码里有个名为「验收」的命令,会绕过密码和双重验证,直接凭空签发一张系统管理员凭证。另一个同类命令会清空审计计数表、修改全局审计策略。
后果这些命令外形和测试工具一模一样,很容易被误当作测试脚本执行。
已列入红线清单(共 46 条禁止执行的命令)
详情
缺陷记录

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

调试脚本残留在公网可访问位置

能修有个网页会泄露服务器配置信息

什么意思一个开发时用的调试脚本留在了公开目录,任何人访问就能看到 PHP 版本、被禁用的函数列表和一段框架源码。
后果属于典型的信息泄露,攻击者用它探测可利用的入口。
删除即可,1 分钟
详情
缺陷记录

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 未加安全标记

能修登录信息可能以明文在网络上传输

什么意思配置里强制 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

审计日志可被应用账号修改删除

能修出了事,记录可能已经被改过

什么意思应用连接数据库用的账号,对审计日志表拥有修改和删除权限。
后果审计记录失去证明力——这在合规检查里是硬性要求。
需要甲方 IT 配合调整数据库账号权限
详情
缺陷记录

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 分单项中位耗时
5 组每项回归测试

系统已在本机完整启动,登录、双重验证、权限控制、审计记录全链路验证通过。每一处修复都有修改前后的实测输出,不是方案建议。

修复速度是实测的

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 美元/席/月年付)。

责任方

交付方 · 上线前必须

二、技术工作(我们自己做)

我们自己能做 19找对方要 6需配合 4甲方准备 11
上线前 40 件事的归属:一半是我们自己的活

最大的一块: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 · 详细资料

给技术同事看的

145代码问题总数
共 145 项,17 项不修不能上线
四项安全修复的完整技术记录(含修复前后实测输出)
CSRF 防护全局启用(含 secureheaders / invalidcha

方案

【结论】三个全局过滤器已启用并跑通:跨站伪造请求由 200 变 403,后台登录+MFA、二次确认、内容写、受限资产交付、官网前台 17 项回归全过。

【方案】按「凭证类型」做非对称防护,而不是给所有 API 一刀切上 token。判定链(Config\Filters.php 末尾的 CsrfGuard):

  1. GET/HEAD/OPTIONS/TRACE 直接放行(与框架 Security::verify 口径一致)。
  2. 请求不带「浏览器自动附带的凭证」(w2r_cms_session、jwt_token cookie)→ 放行。纯 Bearer 客户端、服务端集成完全不受影响:浏览器不会自动加 Authorization 头,构不成 CSRF。
  3. 带 cookie 的写请求先过 Origin/Referer 同源校验(对比请求自身 Host + App::$baseURL 主机 + Security::$trustedOrigins)。Origin: null(沙箱 iframe)判为不可信。两个头都没有 → 判为非浏览器客户端,交给第 4 层。
  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 服务器时的注意事项

  1. 无 schema 变更、无新增迁移、无种子数据。直接覆盖这 5 个 PHP 文件即可,不需要 DBA 配合,不需要停机。回滚就是把文件换回去(备份在 scratchpad/sess_bak/*.orig,含 md5)。
  1. 多实例安全:判定依据全部来自 MySQL(users 与 user_sessions 两张共享表),没有引入任何依赖单机本地状态的东西。实例 A 上的登出/踢人/禁用/改角色,实例 B 下一个请求就生效。nginx + PHP-FPM 多实例、容器多副本都成立。
  1. 刻意不加缓存,请勿「优化」成缓存:当前 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 句柄做这件事。
  1. 请求内备忘(static $accessMemo)在 PHP-FPM / mod_php 下天然按请求隔离;key 里带了 $_SERVER['REQUEST_TIME_FLOAT'],Swoole / RoadRunner 这类常驻 worker 也不会跨请求复用,且超过 64 条自动清空。CLI(spark)不走过滤器,不触发此逻辑。
  1. 上线当天的一次性影响:任何「已签发但 jti 不在 user_sessions 里」的 mfa_verified token 会被判 session_unknown 并要求重新登录。VW 全新部署不存在这类 token;若是在已有环境热更新,等于让当前在线的管理员重新登录一次,建议放在低峰。
  1. 与验收命令的冲突(重要):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,或改成走真实登录流程——文件不在我可改清单内。
  1. 时区坑:user_sessions.revoked_at 由 PHP 侧 date('Y-m-d H:i:s') 按应用时区(Asia/Shanghai)写入,而库里另有一些行是 MySQL NOW()(UTC)写的,同一列存在 8 小时差。我的判定只用 IS NULL / IS NOT NULL,不做任何时间比较,所以不受影响;后续若有人要加「撤销后 N 分钟内…」这类时间窗逻辑,必须先统一这一列的时区来源。
  1. PHP Session 仍是 FileHandler(Config\Session::$savePath = WRITEPATH.'session'),是每实例本地状态。二次确认码(ConfirmTokenService)、受限资产交付(AssetDeliveryController)以及 AuthFilter 的 Session 兜底鉴权都依赖它。这不是本次引入的,但多实例部署必须配置会话粘滞(ip_hash / sticky cookie)或换共享会话存储(Redis/数据库),否则这三条链路会随机失败。
  1. 审计写入量:每次会话级拒绝写一条 operation_logs(operation=session_reject)。触发前提是管理员做过登出/踢人/禁用/改角色,量很小;前端收到 1001 会立刻清 token 跳登录页,实测一次拒绝只产生 1-2 条。非浏览器客户端若拿着已撤销 token 死循环重试,会按请求数写入,运维侧可在 SIEM 上对 session_reject 做频次告警(这本身也是有价值的信号)。
  1. 建议同时确认 config 表里 auth.allow_concurrent_
MFA 密钥与备用码加密存储(AuthService.php:110-112 写

方案

两类数据分开处理,因为它们的业务需求不同。

一、TOTP 密钥 —— 必须可逆,所以做真加密。系统要用它算 TOTP、未完成注册时还要重出二维码,只能可逆加密。选的是 PHP 原生 openssl 的 AES-256-GCM,没用 CI4 自带的 CodeIgniter\Encryption\Encryption,理由有三条,都是可验证的事实而不是偏好:

  1. 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 都表达不了这件事。
  2. CI4 的密文里没有密钥标识(kid),轮换时无法判断某一行是用哪把密钥加的,只能靠「挨个试」。我们的信封格式 mfa1:<kid>:<base64(iv|tag|ct)> 自带 kid,轮换是确定性的机械操作。
  3. 没选 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):

  1. 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 掉首尾空白)。

  1. encryption.keys —— 内联 JSON 多密钥,配 encryption.activeKid。适合密钥只走环境变量、不落盘的场景。
  2. encryption.key —— CI4 传统单密钥。kid 默认 k1,可用 encryption.activeKid 给它命名。
  3. 开发兜底 —— 仅当 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 挂载;不要把密钥烤进镜像层。

【三、发布顺序(多实例下这个顺序不能反)】

  1. 先给所有实例配好密钥(此时老代码不读它,无副作用)。
  2. 再把新代码发到所有实例。新代码读得懂旧的 base64 格式(encryption.mfaAllowLegacy 默认 true),

所以灰度期间新老实例共存不会互相打架。

  1. 全部实例都换成新代码之后,再跑一次 php spark migrate(本迁移幂等,重跑安全)。

反过来先跑迁移会让还没更新的实例读不懂新格式 —— 那一刻起没换代码的实例上谁都过不了 MFA。 代码层面也是按这个假设写的:读路径刻意不做「遗留 base64 -> 密文」的自动升级,只做 kid 轮换的自愈重封装, 就是为了不让灰度期间的某个新实例把行改成老实例读不懂的样子。

  1. 迁移完成、观察一两天没有 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 服务器时的注意事项

  1. 无单机本地状态依赖。策略读 config 表(所有实例共享同一 MySQL),优先级 config 表 > .env > PHP 配置文件。多实例(nginx+PHP-FPM 集群或多容器)只需改一次 config 表,全实例立即一致,不需要逐台改 .env,不需要重启、不需要 reload PHP-FPM。缓存只在单次请求内(Config 类的 static,请求结束即释放),不存在实例间脏读窗口,唯一延迟来源是 MySQL 主从复制。
  1. 切换方案的标准运维动作(三选一,推荐第一种):

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 表则不必)。

  1. 启用 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:* 的角色看不到栏目菜单入口。

  1. 严禁在已上线环境执行 php spark db:seed App\Database\Seeds\RolePermissionSeeder —— 该 run() 会先 DELETE channel_role_permissions / role_permissions / permissions / roles 再重建,等于清空全部授权。已在该方法上加了【危险】注释,线上新增权限码请一律走 ensureChannelManagePermission() 或等价 SQL。
  1. 性能:每个栏目请求新增最多 3 次轻量查询(config 两键 + permissions 全表 22 行)加 1 次用户权限查询,且同一请求内只查一次。若 VW 侧要求进一步压减,把 ChannelAcl 的 $allowDbOverride 设为 false,退化为纯文件配置(届时切换方案需发版)。
  1. 配置基线锁定:等保配置管理若要求「策略不得运行时变更」,把 $allowDbOverride = false 即可,此时 config 表里的 channel.acl.* 一律被忽略,只认发版内容。
  1. 观察到但不属于本任务: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《网络安全等级保护定级指南》的两个定级要素:受侵害的客体、对客体的侵害程度。

支持二级的论据(我实测确认成立,但不足以定二级):

  1. 不处理公民个人信息 —— 实证:官网前台 demo.w2r.site/frontend/src<input> 元素 0 个,无表单、无留资、无线索采集;vgc.users 表字段仅 username / email / password_hash / role / is_active / 三个时间戳,是内部管理员账号,不是公民个人信息。这是二级论据里唯一站得住的一条。
  2. 大概率不属于关键信息基础设施 —— CII 认定由行业主管部门按《关键信息基础设施安全保护条例》做,汽车制造虽属「重要行业和领域」,但对外宣传官网通常不进 CII 清单。

支持三级的论据(更重):

  1. 受侵害客体不止「法人合法权益」。大众汽车集团(中国)是在华合资体量最大的外资车企之一,官网是其唯一官方对外发布渠道。CMS 的核心风险就是「改内容」——首页/新闻被篡改后发布伪造的召回公告、安全声明或官方立场,会被媒体即时放大,可能引发消费者恐慌、经销商体系混乱、母公司股价波动。这已经越过「仅损害法人权益」,落到「损害社会秩序和公共利益」。
  2. 侵害程度是「严重损害」而非「一般损害」。按定级矩阵:客体=社会秩序和公共利益 + 程度=一般损害 → 二级;+ 严重损害 → 三级。汽车行业头部品牌官网的内容篡改事件,其社会影响面难以论证为「一般」。
  3. 行业实践:主机厂面向公众的门户/官网普遍按三级备案,这是专家评审会的默认预期。定二级需要额外举证,反而增加评审不通过的风险。
  4. 系统自己的实现已经超过二级。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 条的 $proxyIPsX-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⁠)
A2w2r.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/cachelogssession` 连启动都会报错同上服务器
A3config 表业务配置数据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/
A5Sitecore 侧源数据导出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. 版本控制:三个仓库都不完整,一个站点根本没进仓库

#缺失项证据影响谁持有
B1GitLab 三仓库的访问权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 开通
B2demo.w2r.site 完全不在版本控制内`unzip -l demo.w2r.site.zip \grep -c "/\.git/" = 0⁠;ls demo.w2r.site/frontend/.gitbackend/.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 上的代码不等于交付的代码,也不等于线上代码。三者之间的差异无人能核对交付方本地 / 服务器工作树
B4docs/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 backend31 份文档与 12 个迁移脚本从未进过任何仓库⁠,只存在于这份 zip 和服务器磁盘上。一旦 zip 丢失即彻底丢失仅此 zip
B5根仓库是备份目录而非活动仓库目录名 w2r.site/.git_root_backup_20260521_174151⁠,最后提交 32f1ab54 Update files根级 git status 无法直接跑,交接方拿到的不是可用工作树

C. .cursor 规范与知识体系(文档第四章有完整清单,实交付近乎为零)

#缺失项证据影响谁持有
C1w2r.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/
C2w2r.site/.cursor/skills/ 10 个 skills清单见 docs/project-introduction.md:288-299⁠;docs/cms/security-model.md 等处引用 .cursor/skills/cms-security-audit/SKILL.md⁠,交付树中不存在同上同上
C3workspace 级 /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 一级目录整个缺席,且交付内文档自相矛盾

#缺失项证据影响谁持有
D1workspace 级 docs/changelog.mdw2r.site/docs/changelog.md:87⁠、:168 两处「见 workspace docs/changelog.md⁠」,用于解释「已发布内容通过修订稿编辑」与 page-css 404 根因关键变更的完整说明缺失,只剩指针服务器 /www/wwwroot/docs/
D2workspace docs/demo-video-media-delivery.mdw2r.site/docs/changelog.md:237docs/cms/video-delivery-and-transcoding.md:4(相对路径 ../../../docs/⁠)均指向它,内容为「批量队列下载」前台交互视频 720p/1080p 交付与批量下载的前台实现说明缺失同上
D3workspace docs/demo-media-permissions-mfa.mdw2r.site/docs/changelog.md:266媒体账号注册 → 后台审批 → 激活 → MFA 的完整规则缺失同上
D4workspace docs/figma-designer-checklist.mddocs/cms/figma-import.md:5「设计侧交付规范(图层命名、Auto Layout、导入不支持的效果)见 Workspace…」设计师如何出稿才能被 Figma 导入正确解析——这条链路的规范没了,后续设计返工风险高同上
D5docs/checklist.xlsxdocs/cms/checklist-implementation.md:3⁠、:48⁠、:53 反复引用,全表 53 行是客户方的安全检查表合规自查的原始表格缺失,checklist-implementation.md 的 B/C 列无处可贴甲方或交付方
D6docs/checklist-mapping.csvdocs/cms/checklist-implementation.md:4⁠、:49同上同上
D7变更记录停在 2026-06-21,代码到 2026-07-22docs/changelog.md 最新条目为 ## 2026-06-21⁠;w2r.site/backend/app/Controllers/Api/ContentController.phpadmin-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机器可读接口规范findopenapi⁠、swagger⁠、postman⁠。接口文档只有两个二进制 Word:docs/Interface-Agreement.docx(38.9 KB,推测为接口约定)、docs/Interface-List.docx(39.7 KB,推测为接口清单),且无 Markdown 源稿(其余 .docx 均有同名 .md)接口无法自动校验、无法生成客户端、无法做契约测试;Word 一旦与代码不符没人发现交付方(生成脚本亦缺失,见 E4)
D11dev-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 没写。

#缺失项证据影响谁持有
E1scripts/import_vgc_news.pyscripts/README.md:181 有完整章节;docs/import_vgc_news.md:4 指名此脚本。目录里只剩它的依赖文件 scripts/requirements-import_vgc_news.txt官网新闻缩略图导入 DAM 的主脚本没了,asset_entry_i18n 关联无法重建交付方
E2scripts/import_mediacenter_news.pyscripts/README.md:252⁠、:274⁠、:281-282媒体中心新闻导入不可复现交付方
E3scripts/md_to_docx.pydocs/README.md:8「由上列 Markdown 经 scripts/md_to_docx.py 生成」;scripts/README.md:293⁠、:308project-introduction.docx 无法从 Markdown 重新生成,Word 与 Markdown 会永久漂移交付方
E4scripts/gen_interface_docs.pyscripts/README.md:315⁠、:340(用 .venv-docx/bin/python 跑)D10 的两份接口 Word 无法重新生成交付方
E5scripts/stats_sitecore_files_import.pydocs/import_sitecore_files.md:86⁠、:89⁠、:92Sitecore 文件导入的结果统计无法复核交付方
E6scripts/migrate_config_v1_to_v2.phpdocs/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⁠,且 .sox86_64-linux-gnu(服务器 venv,本机不可用)文档生成链路(E3 + E4)即使脚本找回来也跑不了交付方

F. 部署与环境:这套代码不知道该怎么上线

#缺失项证据影响谁持有
F1真实站点配置(nginx / 宝塔)全树无 *.conf⁠、无 vhost、无 Dockerfilelocaldev/ 是接手方自建,不算交付物)。交付的改写规则全在 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,需从服务器导出)服务器
F3TLS 证书与续期机制demo.w2r.site/.well-known/acme-challenge/nk2ypvpch9dpho1xjfyozelzg70mzqma 表明用 ACME(Let's Encrypt / 宝塔)签发;证书与私钥不在交付内(这点是对的⁠,但必须走单独渠道交接)证书到期无人续服务器管理员
F4生产环境 .env交付的 w2r.site/backend/.env:5CI_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⁠,值不摘录)服务器 / 交付方
F5test / staging 两套环境docs/cms/requirements.md:78-90 强制要求 dev/test/staging/prod 四环境且数据库、上传目录、缓存按环境隔离;交付只有一份 .env⁠,w2r.sitedemo.w2r.site 同机(47.242.46.253)「上 prod 前在 staging 跑验证清单」(requirements.md:107-115⁠)这条流程无法执行甲方 IT
F6配置晋级基线包 config/*.jsondocs/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/exportRoutes.php:97⁠)能导出,但没有一份已知良好的基线可导入新环境需从生产库现导
F7服务器 / 面板 / 数据库账号属凭据类,交付树中不存在(正确做法)无账号则以上所有服务器侧交接无法执行甲方 IT + 交付方
F8构建用 Makefiledemo.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 应用无任何自动化验证交付方
G3CI 无 build / test / deployw2r.site/.gitlab-ci.yml 全文只有 stages: [test, secret-detection] + 引入 Security/SAST.gitlab-ci.ymlSecurity/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:25docs/cms/requirements.md:162 均写「等保二级导向⁠」。全树无任何测评报告、备案证明、渗透报告「导向」≠「已过测评」。上线合规风险敞口⁠:没有任何第三方证据证明达到等保二级甲方合规部门 / 第三方测评机构

H. 官网前台 demo.w2r.site 的三个月代码代差(本次最重发现)

交付的 demo.w2r.site 源码时间停在 2026-03-23backend/public/api.php⁠、frontend/src/router/index.js⁠、frontend/README.md 皆为该日)。而 CMS 的 docs/changelog.md 在 4–6 月记录了大量已在 demo 上验证通过的功能。这些功能在交付的 demo 代码里一律不存在。

#缺失项证据影响谁持有
H1api.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 子页、媒体中心全部不存在同上
H3PreviewPage.vue⁠、hydrateChronologyLayout.jschangelog :174PreviewPage.vue 登录表单改用户名+密码)、:216hydrateChronologyLayout.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.phpfrontend/public/.htaccess 只有 /asset/{id}/api/asset/{id}` 的 302与 H1 互为佐证:交付的是 3 月快照同上
H5shared/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.mdacceptance-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交付方
I4FAQ 主题管理页面后端 Routes.php:107-111faq/topics CRUD;前端只有 views/FaqQuestions.vue(题库),无主题页acceptance-checklist.md:51「可在后台管理 FAQ 主题(双语)」无 UI交付方
I5响应式断点配置与 ?device= 预览参数docs/cms/requirements.md:146-155acceptance-checklist.md:73-76 要求编辑器设备切换、按断点配置组件、预览链接带 `?device=mobile\tablet\desktopRoutes.php` 无 device 参数处理,前端无设备切换器4 条验收项无法通过交付方
I6i18next 未安装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. 第三站点与外部资产

#缺失项证据影响谁持有
J1139.224.64.38 / vgc.digirepub.com 零代码交付docs/project-introduction.md:44 把它标为「生产域名」、:45 管理后台 https://vgc.digirepub.com/AdM/⁠;交付的两个 zip 只来自 47.242.46.253文档声称的生产环境完全没有交付物⁠。这台机器上跑的是什么版本、和交付代码差多少,无从判断甲方 IT / 交付方
J2Figma 设计源文件与团队访问权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)交付方(注册账号)
J5Sitecore 访问凭据的归属与有效期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 完全一致)+ CreateSuperAdmin CLI 命令,足以起一个空库。缺的是数据不是结构⁠。
  • CSP 确实开着⁠:backend/app/Config/App.php:201 CSPEnabled = true⁠,ContentSecurityPolicy.phpscriptSrc / 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.mdcmedia-permissions-mfa.mdc⁠)
注意 docs/project-introduction.md:25docs/cms/requirements.md:162 的措辞是「等保二级导向⁠」——是设计参照,不是已通过测评的结论。多环境隔离恰恰是等保测评会查的项,现状不满足。

2. 环境判定:交付的 .env 是 development,不是 production

这是本议题最硬的一条事实。

文件
w2r.site/backend/.env5CI_ENVIRONMENT = development
demo.w2r.site/backend/.env5CI_ENVIRONMENT = development
w2r.site/backend/env(CI4 官方模板,未使用)17# CI_ENVIRONMENT = production(注释态)

ENVIRONMENT 常量在代码里分叉 11 处,development 与 production 的行为差异:

位置development 行为production 行为
app/Config/Boot/development.php:13-14error_reporting(E_ALL)⁠、display_errors = 1app/Config/Boot/production.php:12-15⁠:display_errors = 0
app/Config/Boot/development.php:24SHOW_DEBUG_BACKTRACE = true(异常页输出完整栈与文件绝对路径)production 不定义此常量
app/Config/Boot/development.php:34CI_DEBUG = trueproduction.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:42threshold = 9(全量)threshold = 4
app/Config/Security.php:85$redirect = false(CSRF 失败返回而非重定向)$redirect = true
app/Config/Jwt.php:70-72JWT 密钥为空时随机生成⁠,进程重启即全体登出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-69realpath() 解析 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/nk2ypvpch9dpho1xjfyozelzg70mzqmaHTTP-01 验证残留,证书由面板签发
明写 nginx 的部署要求demo.w2r.site/frontend/README.md:24「Nginx:将 location ^~ /api/ 转发到能执行 api.php 的配置(与现有 /api/menu 一致)」
静态映射依赖 web serverw2r.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:41cd /www/wwwroot/vgc —— 与库名 vgcw2r.site/backend/.env:26⁠)同源,说明项目搬过目录
跨站点符号链接demo.w2r.site/shared/README.md:8-9ln -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 权限 0777demo.w2r.site/backend/writable/ 及其全部子目录(ls -la 显示 drwxrwxrwx⁠)全局可写
CORS 全开demo.w2r.site/backend/public/api.php:19Access-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/exportPOST config/import⁠,均挂 auth filter;权限点在控制器里判,ConfigPromotionController.php:26config:export⁠)与 :68config:import⁠)。
  • 支持类型:ConfigPromotionService.php:13workflow⁠、seo⁠、rbac⁠、channel_auth⁠、channels⁠。
  • 导出实现::38-45 分发;:110-131 rbac 直连 roles / permissions / role_permissions 三表;:133-144 channel_auth 连 channel_role_permissions⁠;:146-149 channels 全表 findAll()⁠。
  • 导入实现::85-92⁠,其中 rbac 直接拒绝:88「请使用专用脚本或分步导入 channel_auth」),channels 直接拒绝:90「暂未开放」)。真正能导入的只有 workflowseo 两类,外加 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 用它搬配置的风险

  1. 覆盖式写入,无回滚。 ConfigPromotionService.php:209replaceForChannel() 是整栏目授权替换;一次错误的 channel_auth 导入会把该栏目的全部角色权限规则清空重建,且没有备份可回退(见上表)。
  2. dry_run 给假的安全感。 演练返回「校验通过」不代表真实导入会通过,:72-83 根本没跑到 importXxx⁠。
  3. 没有环境归属标记。 导出体(:47-56⁠)里只有 type / version / exported_at⁠,没有源环境标识。目前两站共库(§6),从哪导的、往哪导的,事后只能靠审计日志的 actor 推断(审计写在 ConfigPromotionController.php:40-57:109-122⁠)。
  4. 它搬不动真正难搬的东西。 内容模型、模板、组件白名单、搜索热词/置顶都不在支持类型里;数据库结构靠 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.25347.242.46.253同机
CI_ENVIRONMENTdevelopment.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⁠)同一账号
媒体存储WRITEPATHapp/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.ymlSecurity/Secret-Detection.gitlab-ci.yml(第 12-13、21-22 行),stages 只有 testsecret-detection⁠,没有 build、没有 deploy、没有 environment 定义⁠。部署只能是手工。
  • 仓库边界不完整。 w2r.site/backend/.git/config:10gitlab.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 整个目录没有 .gitfind 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.vuesrc/lib/nav/megaMenuState.js 是同日 16:00,落后 5 分钟⁠。(这是 zip 内保留的 mtime,为推断依据;上线前必须以重新构建为准,构建配置见 w2r.site/admin-frontend/vite.config.js:8base: '/AdM/'⁠)、:11outDir: ../backend/public/AdM⁠)与 package.json:8(build 后删除产物里的 index.php⁠、.htaccess⁠)。)
  • 域名本身就是临时的。 w2r.site 于 2026-02-08 经阿里云万网注册、2027-02-08 到期;生产域名却是 vgc.digirepub.comdocs/project-introduction.md:44⁠)。而 w2r.site/backend/app/Config/App.php:19baseURL 硬编码成 https://w2r.site/⁠,w2r.site/admin-frontend/js/utils/contentPreview.js:2PUBLIC_SITE_ORIGIN 硬编码成 https://demo.w2r.site(会被打进 AdM 包),app/Services/SeoSchemaService.php:20 默认 base_urlhttps://w2r.site⁠,app/Commands/AuditAcceptance.php:26 硬编码 httpHost = 'w2r.site'⁠。换域名不是改配置,是改源码再重新构建。 后台设置页的占位符倒是写着 https://vgc.digirepub.comadmin-frontend/js/views/Settings.vue:206⁠),说明当初预期过要换。

6.3 接手方已经证明「能重建」

localdev/ 是接手流程自建的本机还原(localdev/.env:1 注明「由接手流程自动生成」),不属于交付物,但它是一份可用的环境规格:localdev/Dockerfile:3php:8.2-apache⁠,:17-19intl mbstring mysqli pdo_mysql zip gd exif + imagick⁠,:20rewrite headers⁠,:24-29 放宽 upload_max_filesize / post_max_size / memory_limit / max_execution_time⁠;localdev/vhosts.conf:8:27 分别把 w2r 站点根设为项目根、把 demo 站点根设为 frontend/distAlias /apibackend/public:35⁠)。这套配置能把两站跑起来,可以直接作为测试环境的起点。


7. 生产与测试环境应有的差异表

配置项位置测试 / staging生产现状
CI_ENVIRONMENTbackend/.env:5testingdevelopmentproduction两站都是 development
display_errors / CI_DEBUGapp/Config/Boot/*.php可开必须关开着,有 debugbar 实证
Debug Toolbardemo/app/Config/Events.php:45可开必须关(当前只受 CI_DEBUG 控制)开着
__hot-reload 路由w2r/app/Config/Events.php:41可开不注册生产会注册
app.baseURLapp/Config/App.php:19(硬编码)测试域名生产域名硬编码,须改为读 .envapp.baseURL
PUBLIC_SITE_ORIGINadmin-frontend/js/utils/contentPreview.js:2测试前台域名生产前台域名硬编码进构建产物
forceGlobalSecureRequestsapp/Config/App.php:160可 falsetrue两站均 false
Cookie secureapp/Config/Cookie.php:57可 falsetruefalse
CSPEnabledw2r App.php:201 = true / demo App.php:201 = false一致一致且开启官网前台无 CSP
JWT_SECRETbackend/.env:37独立随机值独立随机值,且与测试不同单一值,字面写着不可用于生产
encryption.keyapp/Config/Encryption.php:24独立独立空串,.env 中无此项
数据库.env:25-31独立实例或独立库名独立实例、独立账号、最小权限两站同实例同库同账号
DAM 存储根app/Config/Dam.php:49-50⁠、demo .env:24独立目录独立目录跨站符号链接共用一份
writable/ 权限0755,属主 = php-fpm 用户0750 或 0755demo 现为 0777
open_basedir.user.ini:1含站点根 + tmp + DAM 根同左,最小化demo 与其符号链接目标冲突(§3.3)
robots.txtbackend/public/robots.txt⁠、demo/backend/public/robots.txtDisallow: /正常放行两份都是全站放行,测试站会被搜索引擎收录并与正式站争排名
CORSdemo/backend/public/api.php:19可宽松白名单*
opcache.enabledemo/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-scheduleRunPublishSchedule.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

注意事项:

  1. cdbackend/ 是必需的⁠,CleanApplicationLogs.php:15 的示例就是这么写的——CI4 的 spark 依赖工作目录定位 app/.env⁠。
  2. cron 用的 PHP 二进制可能不是 php⁠。 宝塔多 PHP 版本共存时需写全路径(如 /www/server/php/82/bin/php⁠)。这条要在服务器上确认。
  3. 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 表会被系统任务淹没,检索性能与等保「可检索」要求都受影响。
  4. 仓库中无 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. 上线所需的服务器清单

#依据说明
1Linux + nginx(或 Apache + mod_rewrite/mod_headers)404.html:6⁠、frontend/README.md:24⁠;localdev/Dockerfile:20若继续用 nginx,所有 .htaccess 规则必须重写为 nginx location 规则⁠——nginx 不读 .htaccess⁠,交付包里那 6 份改写规则在生产上一条都没生效
2PHP 8.2+,扩展 intl mbstring mysqli imagick⁠,另需 gd exif zipbackend/composer.json:13-17⁠;app/Config/Images.php:14(默认 handler gd⁠)、:20libraryPath = /usr/local/bin/convert⁠)putenv 不得在 disable_functions 里(demo/backend/README.md:36-38⁠)
3PHP 上传/执行限制放宽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)
4MySQL 8,每环境独立库与账号.env:25-31⁠;requirements.md:87字符集 utf8mb4⁠;搜索用 FULLTEXT(requirements.md:33⁠)
5ImageMagick 二进制app/Config/Images.php:20 指向 /usr/local/bin/convert路径需与实际安装位置一致,否则图片处理静默失败
6ffmpeg(若启用视频转码)docs/cms/video-delivery-and-transcoding.md:54「依赖 ffmpeg(或同类工具)」app/Commands/ 下无转码 Worker 命令⁠,只有 DeleteVideoMp4Assets.php⁠;需确认视频转码一期是否真的上线
7Node + npm(构建期)admin-frontend/package.json⁠、demo/frontend/package.json两个前端都需重新构建,见 §6.2
8cron§8 三条含全路径 PHP 二进制
9证书与续期.well-known/acme-challenge/ 残留生产域名换成 vgc.digirepub.com 后需重新签发
10磁盘容量DAM 单文件上限 5 GB(app/Config/Dam.php:12⁠)writable/ 未交付,现有媒体体量未知——这是容量规划的最大盲点

11. 上线所需的配置项清单

必须逐项确认,不能沿用交付包里的值:

配置项位置动作
CI_ENVIRONMENTbackend/.env:5production
app.baseURLapp/Config/App.php:19 硬编码改造成读 .env(模板 backend/env:23 已预留 app.baseURL⁠,只是被注释)
PUBLIC_SITE_ORIGINadmin-frontend/js/utils/contentPreview.js:2改造成构建期变量,否则每换域名都要改源码
SEO base_urlapp/Services/SeoSchemaService.php:20(默认值)+ 后台设置页(Settings.vue:206⁠)上线后在后台设置为生产域名
数据库四件套backend/.env:25-31⁠、demo/backend/.env:11-17新库、新账号、新口令,两站不同库
JWT_SECRETbackend/.env:37必须换(现值明文在交付包里,且字面标注 dev)
encryption.keyapp/Config/Encryption.php:24 为空、.env 中无生成并写入 .env
Sitecore 凭据backend/.env:16-18确认是否仍需要⁠;若不需要则删除,若需要则轮换(现已随包泄露)
Figma PATbackend/.env:61同上,轮换或删除
asset.storageRootdemo/backend/.env:24指向生产 DAM 根,并同步调整 demo/.user.ini:1open_basedir
forceGlobalSecureRequestsapp/Config/App.php:160true(或由反代强制 HTTPS 并设 HSTS)
Cookie secureapp/Config/Cookie.php:57true
CSPEnabled(官网)demo/app/Config/App.php:201requirements.md 的 CSP 要求对齐
CORSdemo/backend/public/api.php:19* 收敛为白名单
robots.txt两站 backend/public/robots.txt测试站改 Disallow: /
open_basedir两站 .user.ini:1按新路径重写,并解决 §3.3 的符号链接冲突
opcache.enabledemo/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:162arial.ttf⁠);权限 0755,属主为 php-fpm 用户
/page-css/ 静态映射EntryCssVersionService.php:12⁠、Figma.php:63cssPublicPrefix = /page-css⁠)已有 CI4 路由兜底(Routes.php:16⁠),但文档建议由 web server 直出——两条路径二选一并写进站点配置

12. 搭一套独立测试/预发环境需要什么(最小可行)

  1. 一台独立主机或独立容器⁠,不与生产共享 MySQL 实例与文件系统。localdev/docker-compose.yml + localdev/Dockerfile + localdev/vhosts.conf 已经是一份跑得通的规格,可直接改造为 staging。
  2. 独立数据库⁠:跑 51 个迁移(app/Database/Migrations/⁠)+ RolePermissionSeeder⁠,得到空壳;业务数据必须向交付方索取全库导出⁠,否则无法验证任何真实行为。
  3. 独立 DAM 目录⁠:不能复用生产的 writable/⁠,也不能用符号链接指回生产——这正是当前两站踩的坑。
  4. .env 三份分离⁠:dev / staging / prod 各一份,密钥各不相同,且都不进仓库(.gitignore:12 已排除)。
  5. 把域名与前台 origin 从源码里挖出来(见 §11 前三行),否则 staging 会指向生产前台。
  6. 站点配置纳入版本控制⁠:nginx conf、php-fpm pool、crontab、.user.ini 都要有一份可 diff 的副本。当前 .gitignore:14-15.htaccess.user.ini 排除在外,这个决定要推翻。
  7. 测试站 robots.txtDisallow: /⁠,并加 HTTP Basic 或 IP 白名单——现在 demo.w2r.site 是公网可达且全站放行收录的。
  8. 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_logsip⁠、user_agent2025-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 安全计算环境(应用层,代码能覆盖的部分)

等保要求项二级三级代码是否实现实现在哪差距
身份鉴别 — 唯一标识要求要求usersusername 唯一键(*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 = falseConfig/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.1CSRF 全局过滤器被注释backend/app/Config/Filters.php:80-84// 'csrf', 在 82 行);同时 honeypot⁠、invalidchars⁠、secureheaders 全部注释安全计算环境·访问控制;渗透测试项不符合⁠。需注意这是组合缺陷⁠:系统主用 Authorization 头(浏览器不自动携带,本身不可 CSRF),但 JwtService.php:189-199 还兜底接受 CookieURL query 两种传递方式——Cookie 会被浏览器自动携带,CSRF 于是真实可用。另外 docs/cms/security-model.md:458 写「Cookie 设置 SameSite=Strict 防止 CSRF」,而 Config/Cookie.php:90 实际是 'Lax'⁠,文档与代码不一致⁠,测评时文档核对环节会额外扣分
3.2ContentController 与 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.3user_sessions 撤销表只写不读写:UserSessionService.php:100-108revokeSession⁠)、110-117revokeAllForUser⁠);登出调用点 AuthController.php:298⁠。读:AuthFilter.php:12-35 从不查 revoked_at身份鉴别·超时自动退出;剩余信息保护不符合⁠。后果有两层:① 登出后 token 继续有效,最长到签发后 2 小时(Config/Jwt.php:29 = 7200 秒);② 「禁止并发登录」这个可配置开关(UserSessionService.php:22-27、76-82⁠)形同虚设⁠——撤销了旧会话也没人执行。等于系统认为自己有会话管理,实际没有
3.4MFA 密钥仅 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:34auth_no_mfa(只验密码不验 MFA)身份鉴别·登录失败处理「限制非法登录次数」不符合,倾向高风险项。 我的分析:纯 TOTP 侧,6 位码 + ±1 时间窗(AuthService.php:240-247⁠)单次命中率 3/10⁶,30 秒窗口内暴力不现实;真正可打的是备用码⁠——8 个静态 6 位数字,无时间窗、无使用次数上限、无节流,10⁶ 空间可枚举,命中任一即通过(:221-228⁠)。且 verifyMfaCode 先查备用码再验 TOTP⁠。这条我判为比清单原描述更严重
3.6JWT 可从 URL 查询参数传入JwtService.php:195-199$request->getGet('token')⁠);且这是设计行为⁠,security-model.md:358-363 明确列为第 3 种传递方式身份鉴别·鉴别信息保护;安全审计(日志不得记录鉴别信息)不符合⁠。token 会进 nginx access_log、Referer 头、浏览器历史、被用户复制分享。等保之外,这条也是 OWASP 常规扣分项。修复要连文档一起改
3.7public/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 = developmentbackend/.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 代码层面能做的(交付方 / 接手团队可自己完成,不花钱)

按修复优先级排序,全部是应用代码改动:

  1. backend/.env:37 换成 openssl rand -base64 32 生成的强密钥;:5production⁠;旧密钥签发的 token 全部作废。
  2. ContentController / ChannelControllersecurity-model.md:44-67 补全 requirePermission 调用,参照 DamController 的写法。补完跑一遍 php spark 下已有的 VerifyRolePermission 命令(app/Commands/VerifyRolePermission.php⁠)验证。
  3. AuthFilter 增加 user_sessions.revoked_at 校验(可加短 TTL 缓存避免每请求查库),让登出、并发登录控制、超时退出真正生效。
  4. 关闭 JwtService::getTokenFromRequest 的 query 与 cookie 兜底(JwtService.php:189-199⁠),同步改 security-model.md:358-363⁠。
  5. MFA 种子与备用码改用 CI4 Encryption 服务真加密;备用码改成加盐哈希存储、一次性、并计入节流;AuthService.php:110 的假注释删掉。
  6. mfaVerify 接入 LoginThrottleService⁠,并改为按「账户 + IP」双维度计数。
  7. 密码过期改服务端强制:过期时只发放仅能访问 change-password 的受限 token。
  8. 增加账户级锁定字段与逻辑(连续失败 N 次锁账户,需管理员解锁;解锁动作要审计,audit-logging.md 里已经预留了 user_lock / user_unlock operation)。
  9. 删除 backend/public/test-putenv.php⁠、groupui-showcase.html⁠。
  10. forceGlobalSecureRequests = true⁠、Cookie::$secure = true⁠、samesite = 'Strict'⁠、regenerateDestroy = true⁠、Session::$expiration 与文档对齐。
  11. 恢复 Filters.php 里的 secureheaders⁠;CSRF 视最终 token 传递方案决定是否需要。
  12. CleanAuditLogs 默认保留期由 6 个月改 12 个月,并把 crontab 配置写进部署文档。
  13. security-model.md⁠、logging-system-design.md 里所有「实现建议」「推荐」的措辞逐条判定:要么落地,要么明确标注为未实施。 测评员按文档验证,文档写了没做比没写还糟。

4.2 必须走流程 / 买服务的(代码解决不了)

A. 行政与资质

  • 定级报告编制 + 3—5 名专家评审(甲方组织)
  • 属地公安网安部门备案,取得《备案证明》
  • 选择公安部认可资质的测评机构(可在公安部「等级测评机构推荐目录」查属地机构)
  • 若定三级:商用密码应用安全性评估(密评)⁠,且须在投入运行前完成,运行后每年一次并报市级密码管理部门备案

B. 安全管理制度体系(三级约 30 余项;二级可精简但不能没有)

层级文件
安全策略层网络安全总体方针与安全策略
制度层·管理制度安全管理制度制定与评审管理办法
制度层·机构岗位安全管理机构与岗位职责制度;跨部门安全协调与沟通机制
制度层·人员安全人员安全管理办法;安全教育与培训管理制度;保密与安全责任制度
制度层·系统建设定级备案管理办法;等级测评管理制度;安全建设项目管理规定;安全产品与服务采购管理办法;第三方服务商安全管理制度⁠;系统上线安全验收管理规定;软件开发安全管理办法
制度层·系统运维机房与物理环境安全管理制度;资产管理办法;配置管理制度;存储介质安全管理制度;网络与系统安全运维管理制度;账号与权限管理制度⁠;安全审计管理制度⁠;漏洞与补丁管理办法;恶意代码防护管理制度;变更安全管理办法;数据安全与备份恢复管理制度⁠;密码应用安全管理制度;安全事件处置与报告管理办法;网络安全应急预案⁠;个人信息保护管理制度
操作规程层上述制度的执行细则(约 6 项核心)
记录表单层执行佐证:权限审批单、变更单、备份记录、演练记录、培训签到、日志审查记录

加粗的几项与本项目现状直接冲突,是最先要写的。

C. 安全设备与服务

二级需要三级额外需要
防火墙WAF(互联网系统必需)
IPS 或 IDSIPS(主动阻断)
主机防病毒堡垒机
日志审计(≥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.38vgc.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 定级要素来自公开解读;费用、周期、安全设备清单、制度清单来自测评 / 咨询机构的商业页面,属行业惯例参考,不是法规硬性要求,签合同前须以属地公安与测评机构口径为准⁠。

来源:

大众汽车集团(中国)官网 CMS 交接:需要购买的认证、授权与订阅(26 项)

需要购买的认证、授权与订阅

全部论断标注了 仓库相对路径:行号⁠,或写明「仓库中无此项」。联网核实的部分标注了来源与检索时间(统一为 2026-08-11⁠)。「事实」与「推断」分开写。区分「法规硬性要求」与「行业惯例」。

0. 一句话结论

这个项目在「买什么」这件事上最大的问题不是花多少钱,而是交付给你的服务器在香港,而页脚硬编码的是一个内地备案号⁠。这两件事在合规上互斥。在决定服务器落在哪一侧之前,备案、等保、密评、云资源四项的预算全部是空的。此外交付包里混进了两套需要授权的字体(VW 定制 The Group + Monotype 蒙纳翔鹤黑),而真正的中文字体栈是空的。第三方开源组件这一项反而是全项目最干净的:零 GPL / AGPL / LGPL⁠。


1. 前提:交付的那台机器在境外(这一条决定其余所有条)

IP归属(检索 2026-08-11,ipinfo.io)位置在本项目里的角色
47.242.46.253AS45102 Alibaba (US) Technology Co., Ltd.;ARIN netname ALIBABA-CLOUD---HKHK / 中国香港同机跑 w2r.site 与 demo.w2r.site,交付代码就来自这台
139.224.64.38AS37963 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 现在这行页脚有三个具体缺陷

  1. 写法不规范:BJ ICP 08103718 -21 不是「京ICP备08103718号-21」的法定写法。
  2. 没有链接⁠。备案号需在首页底部标明并链接到 beian.miit.gov.cn⁠;公安备案号需带警徽图标并链接到全国互联网安全管理服务平台(行业通行做法,各接入商备案指引一致要求)。当前是纯文本常量,无 <a>⁠。
  3. 硬编码在前端源码里,改一次要重新构建发版,不走 CMS 配置。

2.3 费用

备案本身不收费(阿里云帮助中心明示,检索 2026-08-11)。花钱的是前置条件:申请免费备案服务码要求 ECS 满足「中国内地地域 + 包年包月(含自动续费)≥ 3 个月 + 已分配公网 IP」。也就是说,备案的真实成本 = 你必须先买一台内地 ECS⁠。


3. 域名

域名事实来源注册商关键日期归属判断
w2r.sitelocaldev/report/shell.html:312阿里云万网,DNS 在 hichina2026-02-08 注册 → 2027-02-08 到期乙方自建演示域名,控制权归属未确认LOG.md:88⁠、Progress.md:18 均记为未确认)
digirepub.comRDAP rdap.verisign.com⁠,检索 2026-08-11Alibaba 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.htmldemo.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:25docs/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):

  1. 关键信息基础设施(CII)运营者⁠;或
  2. 网络安全等级保护第三级及以上信息系统。

满足其一 → 每年至少评估一次,可与等保测评同步进行。

本项目是否触发:按当前口径不触发。 定级为二级、且企业官网通常不被认定为 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)持有,通过品牌门户按内部规则下发,通常不对外单独售卖。

因此这里大概率不需要「买」,但一定需要「确权」⁠:

  1. 这两个 TTF 是从哪里拿到的?是否有 VW Group 品牌部门 / SDC 的书面下发记录?
  2. 品牌规范是否允许在公网站点以 webfont 形式投放?
  3. 以 TTF 明文投放 = 任何访客都能下载到可直接安装的字体文件。 定制企业字体一般要求转 WOFF2、加 subset、限制 referer。这是资产保护问题,不是钱的问题。技术改法明确:fonts.scssformat('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 个 skillsdemo.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 严重得多:

  1. 后端有 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 的高保真导入链路。
  2. 前台有 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.vuesrc/data/mockNewsArticle.js 引用,即新闻详情页的图片源⁠。
  3. 文件自己在开头(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 文件逐包提取,结果只有两种许可证:

许可证包数传染性
MIT46
BSD-3-Clause31

重点关注项(w2r.site/backend/composer.json:12-22 的直接依赖):

组件版本约束许可证结论
CodeIgniter 4 框架本体4.7.2(backend/system/ 手动集成)MITw2r.site/backend/LICENSE:1⁠)可商用,仅需保留版权声明
phpoffice/phpspreadsheet^2.0MIT✅ 干净。注意:它的前身 PHPExcel 是 LGPL⁠,很多人凭印象以为还是 LGPL,实际 PhpSpreadsheet 已改 MIT
firebase/php-jwt^6.10BSD-3-Clause✅ 干净
laminas/laminas-escaper^2.18BSD-3-Clause
psr/log⁠、psr/simple-cache^3.0MIT
predis/predis(dev)^3.0MIT
phpunit/phpunit(dev)`^10.5.16 \\^11.2`BSD-3-Clause✅ 仅 dev
friendsofphp/php-cs-fixer(dev)^3.47.1MIT✅ 仅 dev
Symfony 组件族(22 个,经 php-cs-fixer 传入)全 MIT✅ 仅 dev
ReactPHP 组件族(7 个)全 MIT✅ 仅 dev

9.2 管理后台前端(w2r.site/admin-frontend/package.json:11-29⁠,135 个已安装包)

许可证包数
MIT121
ISC9
BSD-3-Clause3
BSD-2-Clause1
Apache-2.01

非 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.44docs/software-bom.md:19Oracle 自 2023-10-25 起将 5.7 移入 Sustaining Support,实际含义是不再发安全补丁(mysql.com EOL 公告,检索 2026-08-11)。这不是许可证费,是 EOL 风险:要么升 8.0 / 迁 MariaDB(人力成本),要么买商业支持 / 用云厂商的付费延长支持。必须排期。
ffmpegdocs/cms/video-delivery-and-transcoding.md:54(转码 Worker 依赖 ffmpeg)ffmpeg 默认 LGPL、启用 x264/x265 后为 GPL。但本项目是服务端调用、不随产品分发⁠,copyleft 义务不触发(推断,建议法务复核)。H.264/AVC 的专利池授权是另一条线,需甲方法务确认现有 VW 全球协议是否已覆盖。

10. 云资源

10.1 现状(全部为事实)

  • 单机自建,不是托管服务⁠:w2r.sitedemo.w2r.site 同机(47.242.46.253),宝塔面板 + nginx,MySQL 5.7.44 装在同一台。
  • 改写规则写在 Apache 语法的 .htaccessw2r.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,才能申请免费备案服务码
按量付费 ECS0.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 日内)甲方 IT0 元以 ICP 备案号为前置;境外服务器无法办理
页脚备案号写法 + 链接 + 警徽法规强制接手团队改 SiteFooter.vue:51-520 元(改代码)上线前必须
内地 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 迁走)非强制甲方 IT0.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. 三个必须先定、否则整张单子作废的决策

  1. 生产服务器落在内地还是境外? 内地 → 备案链条全套启动、内地 ECS 必买、页脚号要新增;境外 → 备案不做也做不了,那页脚那行必须删掉或改写,不能留着一个不属于本域名的备案号。
  2. 正式域名用哪个? 用乙方的 vgc.digirepub.com 等于把生产入口交给代理商;用甲方自有的 volkswagengroupchina.com.cn 才是可持续形态,但那要走替换现网 Sitecore 的完整切换。
  3. 等保定几级? 二级和三级之间差着一年一次的强制测评、差着密评是否触发、差着 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 专利池授权是另一条独立的线。

交付的代码来自这一台阿里云 · 开发演示机47.242.46.253w2r.siteCMS 后台 · baseURL 指向它demo.w2r.site官网前台 · 同机兄弟目录文档标注的生产环境 · 无任何代码另一台服务器139.224.64.38vgc.digirepub.com归属未确认被替代的现有官网现网 · Sitecore211.151.111.91volkswagengroupchina.com.cn迁移数据来源
三台服务器分属不同位置

2026-08-12 · 全部结论来自本机实测,每条附文件与行号
图为示意,数据图按真实数字生成