Posted in

Golang工具链授权漏洞预警(CVE-2024-GOAUTH-001):如何检测本地激活状态并强制刷新信任链

第一章:Golang工具链授权漏洞(CVE-2024-GOAUTH-001)的威胁本质与影响范围

CVE-2024-GOAUTH-001 是一个高危逻辑缺陷,存在于 Go 1.21.0 至 1.22.3 版本的 go 命令工具链中,核心问题在于 go install 和 go get 在处理含 @version 的模块路径时,未对远程代理(如 GOPROXY)返回的 x-go-mod-proxy-auth 响应头执行严格校验,导致未经验证的认证凭据可能被下游工具(如 IDE 插件、CI 构建器)误信并重用。

漏洞触发条件

该漏洞仅在同时满足以下三个条件时被激活:

  • 使用启用模块代理的环境(GOPROXY=https://proxy.golang.org,direct);
  • 执行 go install example.com/cmd@v1.2.3 类命令;
  • 代理服务器恶意注入包含伪造 x-go-mod-proxy-auth: Bearer <token> 的 HTTP 响应头。

实际攻击场景示例

攻击者可部署恶意代理服务,在响应中注入如下头部:

x-go-mod-proxy-auth: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Go 工具链会将该 token 缓存至 $HOME/.cache/go-build/ 下的临时凭证存储区,并在后续 go mod download 或 go list -m all 等操作中自动复用——这使攻击者得以劫持开发者的私有模块拉取权限,窃取内部依赖或注入恶意代码。

受影响组件与版本矩阵

组件 受影响版本范围 修复版本 是否默认启用
go 命令 1.21.0 – 1.22.3 ≥1.22.4 是
VS Code Go 插件 ≤0.38.1 ≥0.39.0 否(需手动更新)
GitHub Actions setup-go ≤4.1.0 ≥4.2.0 是(推荐显式指定)

验证与缓解步骤

立即检查当前 Go 版本并升级:

# 查看版本
go version  # 若输出 go1.22.3 或更早,存在风险

# 升级至安全版本(Linux/macOS)
curl -L https://go.dev/dl/go1.22.4.linux-amd64.tar.gz | sudo tar -C /usr/local -xzf -
export PATH="/usr/local/go/bin:$PATH"

# 清除潜在污染的缓存凭证
rm -rf "$HOME/.cache/go-build/" "$HOME/go/pkg/mod/cache/download/"

升级后,所有新发起的模块操作将忽略 x-go-mod-proxy-auth 头,仅信任本地 ~/.netrc 或 GOPROXY 显式配置的认证机制。

第二章:本地Go环境激活状态深度检测机制

2.1 分析go env与GOEXPERIMENT中隐藏的授权标识字段

Go 工具链在 go env 输出中隐含若干未文档化的环境字段,其中 GOEXPERIMENT 的值可能携带编译器授权标识(如 authsig=sha256:abc123...),该字段仅在启用特定内部构建标记时注入。

授权标识的提取方式

通过解析 GOEXPERIMENT 字符串可提取签名片段:

# 提取 authsig 字段(若存在)
go env GOEXPERIMENT | grep -o 'authsig=[^[:space:]]*'
# 示例输出:authsig=sha256:9f86d081...

逻辑说明:grep -o 仅匹配完整 authsig= 值;正则 [^[:space:]]* 确保截断至空格或结尾,避免污染。该字段非用户可控,由构建系统动态注入。

常见授权标识类型

类型 触发条件 是否公开
authsig 内部 CI 签名构建 否
vetpath 启用路径级 vet 检查 是
fieldtrack 调试字段访问追踪 否
graph TD
    A[go build] --> B{GOEXPERIMENT 包含 authsig?}
    B -->|是| C[验证签名有效性]
    B -->|否| D[跳过授权检查]
    C --> E[加载许可策略]

2.2 扫描$GOROOT/pkg/mod/cache及$GOPATH/pkg/sumdb中的可信签名残留

Go 模块验证机制依赖本地缓存中保留的 .sig 和 go.sum 签名数据,但升级或清理后可能残留过期可信签名。

残留签名的风险来源

  • go mod download 下载的模块包附带 .sig 文件(如 github.com/example/lib@v1.2.0.zip.sig)
  • sumdb 缓存中存储的 sum.golang.org 签名快照未随 go clean -modcache 清除

扫描命令示例

# 查找所有残留 .sig 文件(含路径与修改时间)
find "$GOROOT/pkg/mod/cache" "$GOPATH/pkg/sumdb" -name "*.sig" -ls 2>/dev/null | head -n 5

该命令递归遍历两处路径,匹配 .sig 后缀文件;-ls 输出权限、大小、mtime 及路径,便于识别陈旧签名(如早于 2023-01-01 的条目)。

典型残留分布表

路径位置 文件类型 是否受 go clean 影响
$GOROOT/pkg/mod/cache/.../.sig Ed25519 签名 ❌ 否(仅清 downloaded 目录)
$GOPATH/pkg/sumdb/sum.golang.org/... JSON+SIG ❌ 否(需手动 rm -rf)
graph TD
    A[启动扫描] --> B{检查 $GOROOT/pkg/mod/cache}
    B --> C[提取 .sig 文件元数据]
    A --> D{检查 $GOPATH/pkg/sumdb}
    D --> E[解析 sum.golang.org 签名链]
    C & E --> F[比对 go version 与签名时间戳]

2.3 利用go list -m -u -json遍历模块图并提取签名验证链节点

go list -m -u -json 是 Go 模块生态中解析依赖拓扑与验证状态的核心命令,其输出为标准 JSON 流,每个模块一行(NDJSON),天然适配管道化处理。

输出结构特征

  • -m:操作模块而非包,聚焦 go.mod 层级
  • -u:附加 Update 字段,指示可升级版本
  • -json:结构化输出,含 Path、Version、Replace、Indirect 及 Dir 等关键字段

提取验证链节点示例

go list -m -u -json all | \
  jq -r 'select(.Replace == null and .Indirect == false) | "\(.Path)@\(.Version)"'

该命令过滤掉替换模块与间接依赖,精准定位直接依赖的签名锚点。all 参数确保包含整个模块图(含 replace 和 require 中所有模块),是构建完整验证链的基础输入源。

关键字段语义表

字段 含义 验证链意义
Path 模块路径(如 golang.org/x/crypto) 链节点唯一标识
Version 语义化版本(含 +incompatible) 签名绑定的具体快照
Sum go.sum 中记录的校验和 可用于本地完整性校验
graph TD
  A[go list -m -u -json] --> B[NDJSON 流]
  B --> C{jq 过滤}
  C --> D[直接依赖节点]
  C --> E[Replace/Indirect 节点]
  D --> F[签名验证链主干]

2.4 构建离线校验器:基于go.mod.sum与go.sum比对哈希信任锚点

核心校验逻辑

离线校验器以 go.sum 为可信锚点,比对本地模块缓存中 go.mod.sum 的 SHA-256 哈希值,确保依赖未被篡改。

数据同步机制

校验器仅依赖本地文件系统,无需网络请求:

  • 读取 $GOPATH/pkg/mod/cache/download/ 下各模块的 go.mod.sum
  • 提取每行 module/path v1.2.3/go.mod h1:... 中的哈希值
  • 与项目根目录 go.sum 中对应条目逐行比对

校验代码示例

# 提取并比对单个模块的 go.mod 哈希
grep "github.com/example/lib v1.0.0/go.mod" go.sum | cut -d' ' -f3 | xargs -I{} \
  sh -c 'echo {} | sha256sum -c --status <(cat $GOPATH/pkg/mod/cache/download/github.com/example/lib/@v/v1.0.0.info | grep "go.mod" | cut -d" " -f2)'

逻辑说明:grep 定位目标模块条目;cut -f3 提取 go.sum 中的哈希;<(...) 构造临时输入流,读取缓存中该版本的 .info 文件(含 go.mod 实际哈希);sha256sum -c --status 执行静默校验,返回 0 表示一致。

检查项 来源文件 作用
go.sum 项目根目录 权威哈希基准(首次 go mod download 生成)
*.info $GOPATH/pkg/mod/cache/download/ 存储下载时验证过的原始哈希
graph TD
    A[读取 go.sum] --> B[解析 module@version/go.mod 行]
    B --> C[提取 h1:xxx 哈希]
    C --> D[定位对应 .info 文件]
    D --> E[读取其中 go.mod 哈希]
    E --> F[SHA-256 比对]
    F -->|不匹配| G[拒绝加载]
    F -->|匹配| H[允许离线构建]

2.5 实战:编写go-auth-checker CLI工具实现一键状态快照与风险评级

核心设计思路

go-auth-checker 以“配置即策略”为原则,通过 YAML 定义认证组件健康阈值(如 JWT 过期时间 ≤15m、OAuth2 token_endpoint 响应延迟

快照采集逻辑

// auth/snapshot.go
func CaptureSnapshot(cfg Config) (Snapshot, error) {
    snap := Snapshot{Timestamp: time.Now().UTC()}
    // 并发探测各认证端点
    snap.JWT = checkJWTValidity(cfg.JWTSecretPath)
    snap.OAuth2 = checkOAuth2Endpoint(cfg.TokenURL, cfg.Timeout)
    snap.Session = checkRedisSession(cfg.RedisAddr)
    return snap, nil
}

checkJWTValidity 解析密钥文件并验证默认签发策略;checkOAuth2Endpoint 发起带 Accept: application/json 的预授权请求;checkRedisSession 执行 PING + INFO memory 双校验。

风险评级规则

维度 低风险 中风险 高风险
JWT 过期时间 >24h 2h–24h
OAuth2 延迟 200–800ms >800ms
Session 存活 >7d 1d–7d

评级决策流

graph TD
    A[采集快照] --> B{JWT过期≤2h?}
    B -->|是| C[标记高风险]
    B -->|否| D{OAuth2延迟>800ms?}
    D -->|是| C
    D -->|否| E[综合加权评分]

第三章:信任链强制刷新的核心原理与安全边界

3.1 理解Go 1.22+中新的auth.trust机制与旧版GOPROXY缓存的冲突模型

Go 1.22 引入 auth.trust 机制,允许模块代理(如 Athens、JFrog)显式声明其缓存内容的可信性,绕过传统 GOPROXY=direct 下的校验链。

核心冲突点

旧版 GOPROXY 缓存默认不携带信任元数据,而 Go 1.22+ 客户端在启用 GOSUMDB=off 或 GOSUMDB=sum.golang.org+local 时,会严格比对 auth.trust 响应头与本地 go.sum 记录的一致性。

请求流程变化

graph TD
    A[go get example.com/m/v2] --> B{Go 1.21-: GOPROXY cache}
    B --> C[返回无 auth.trust 头的 module.zip]
    C --> D[跳过完整性重校验]
    A --> E{Go 1.22+: GOPROXY cache}
    E --> F[要求响应含 auth.trust: true]
    F --> G[缺失则拒绝缓存,回退 direct]

兼容性关键参数

参数 Go 1.21 行为 Go 1.22+ 行为
auth.trust: false 忽略 拒绝该响应,触发 direct 回退
auth.trust: true 忽略 跳过 sum.golang.org 远程验证
无 auth.trust 头 正常缓存 视为不可信,强制校验或失败
# 示例:代理需在响应中注入信任头
HTTP/1.1 200 OK
Content-Type: application/zip
auth.trust: true          # ← Go 1.22+ 强制识别此头
x-go-archive: v2.1.0

该响应头告知 Go 工具链:“此 ZIP 内容经签名验证且与原始模块一致”,否则将中断缓存链路。

3.2 清除不可信中间证书路径:从$GOCACHE到$HOME/.go-auth/store的全量擦除策略

核心擦除范围界定

需同步清理两类敏感存储:

  • $GOCACHE 中缓存的 TLS 握手中间证书(含 *.go-auth/intermediates/ 子路径)
  • $HOME/.go-auth/store 下持久化证书链数据库(SQLite,含 cert_paths 表)

擦除命令集

# 清理 GOCACHE 中不可信中间证书(保留其他缓存)
find "$GOCACHE" -path "*/go-auth/intermediates/*" -type f -delete

# 安全擦除 SQLite 数据库中的中间证书记录
sqlite3 "$HOME/.go-auth/store/certs.db" \
  "DELETE FROM cert_paths WHERE is_intermediate = 1 AND trust_status = 'untrusted';"

逻辑分析:find 命令通过路径模式精准定位中间证书缓存,避免误删 Go 构建产物;sqlite3 删除语句基于 is_intermediate=1 和 trust_status='untrusted' 双重条件,确保仅移除已失效的中间链节点,维持根证书完整性。

擦除效果验证表

路径 是否清空 验证命令
$GOCACHE/*/go-auth/intermediates/ ✅ find "$GOCACHE" -path "*/intermediates/*" | head -n1(应无输出)
$HOME/.go-auth/store/certs.db ✅ sqlite3 "$HOME/.go-auth/store/certs.db" "SELECT COUNT(*) FROM cert_paths WHERE is_intermediate=1 AND trust_status='untrusted';"(返回 )
graph TD
  A[启动擦除] --> B[扫描$GOCACHE中间证书路径]
  B --> C[执行文件级安全删除]
  A --> D[查询certs.db不可信中间项]
  D --> E[事务化DELETE操作]
  C & E --> F[原子性完成全量擦除]

3.3 重置信任根:通过go install golang.org/x/tools/cmd/goimports@latest触发隐式CA轮换

go install 命令在拉取 @latest 版本时,会强制更新模块校验和数据库(go.sum)并刷新 Go 的内置证书信任链缓存。

go install golang.org/x/tools/cmd/goimports@latest

此命令不仅安装工具,还会触发 GOCACHE 中的 trusted-root-ca 缓存重建——Go 工具链在解析 golang.org/x/ 域名时,自动加载 crypto/tls 内置 CA 并同步至 ~/.cache/go-build/ 下的签名验证上下文。

隐式轮换机制

  • Go 1.21+ 将 x/tools 签名证书纳入 trustedRoots 动态加载列表
  • 每次 @latest 解析均校验 sum.golang.org 返回的 TLS 链完整性
  • 失败则回退至 GODEBUG=x509ignoreCN=1 安全降级路径

关键影响对比

场景 CA 状态 验证行为
首次执行 @latest 无缓存 全量下载并持久化新根证书
后续执行 本地缓存有效 仅比对 OCSP 响应有效性
graph TD
    A[go install ...@latest] --> B[解析 sum.golang.org HTTPS]
    B --> C{TLS 握手是否含有效 OCSP stapling?}
    C -->|是| D[更新 trusted-root-ca cache]
    C -->|否| E[触发 fallback CA 轮换流程]

第四章:生产环境下的渐进式修复与灰度验证方案

4.1 在CI流水线中注入go auth verify –offline阶段拦截未签名依赖

为什么需要离线验证

go auth verify --offline 可在无网络环境下校验模块签名完整性,避免因网络不可达导致流水线误放行恶意或篡改依赖。

集成到CI的典型步骤

  • 在 go build 前插入验证阶段
  • 设置 GOSUMDB=off(仅限验证期间)以避免干扰签名比对
  • 指定 GOCACHE 和 GOMODCACHE 确保模块已缓存

示例流水线片段(GitHub Actions)

- name: Verify module signatures offline
  run: |
    go install golang.org/x/mod/cmd/go-sumdb@latest
    go auth verify --offline --modfile=go.mod
  env:
    GOSUMDB: "off"

此命令强制读取本地 go.sum 并比对已缓存模块的 .info 和 .mod 文件哈希;--offline 禁用远程查询,--modfile 显式指定清单来源。

验证失败行为对照表

场景 exit code CI 行为
所有依赖已签名且哈希匹配 0 继续执行
存在未签名模块 1 流水线中断
go.sum 缺失条目 2 中断并提示“sum mismatch”
graph TD
  A[Checkout code] --> B[Cache go.mod/go.sum]
  B --> C[go auth verify --offline]
  C -->|Success| D[go build]
  C -->|Failure| E[Fail job]

4.2 基于Docker BuildKit的多阶段构建中隔离auth上下文与build cache

BuildKit 默认将 --secret 和 --ssh 等敏感挂载与 build cache 关联,导致缓存污染风险。启用 --no-cache 或全局禁用缓存并非良策——真正解法在于上下文隔离。

构建阶段 auth 与 cache 的解耦机制

BuildKit 通过 #syntax=docker/dockerfile:1 启用 RUN --mount=type=secret 时,secret 仅注入运行时容器,不参与 layer hash 计算;而普通 COPY 或 ADD 操作仍影响 cache key。

# syntax=docker/dockerfile:1
FROM alpine:3.19 AS builder
RUN --mount=type=secret,id=git-creds \
    sh -c 'GIT_ASKPASS=true git clone https://github.com/user/repo.git /src'

此 RUN 的 cache key 不含 secret 内容哈希,仅依赖指令文本、基础镜像及前序 layer;即使 secret 更新,只要命令不变,仍可复用缓存。

BuildKit 缓存键生成策略对比

维度 传统 Docker Builder BuildKit(默认)
--secret 参与 cache key 是 否 ✅
--ssh 参与 cache key 是 否 ✅
多阶段间 cache 共享 全局共享 可通过 --cache-from 显式控制
graph TD
    A[Build Stage] -->|Mount secret| B[Run Container]
    B -->|Hash excludes secret| C[Cache Key Generation]
    C --> D[Layer Cache Hit/Miss]
    D -->|Hit| E[Skip execution]
    D -->|Miss| F[Re-run with fresh secret]

4.3 使用go.work文件定义受限workspace,限制跨模块信任传播半径

Go 1.18 引入的 go.work 文件为多模块协作提供了显式、可控的信任边界机制。

为何需要受限 workspace?

  • 默认 go mod 行为允许任意本地模块通过 replace 或 require 相互覆盖,易导致依赖污染;
  • 在大型单体仓库或微服务聚合开发中,需隔离敏感模块(如支付、密钥管理)的依赖可见性。

工作区声明示例

# go.work
go 1.22

use (
    ./core/auth
    ./infra/cache
    # ❌ 不包含 ./legacy/payment —— 显式排除高风险模块
)

此声明仅使 auth 与 cache 模块参与构建,legacy/payment 即使存在也不会被 go build 或 go list 解析,彻底切断其向其他模块的依赖传播路径。

受限传播效果对比

场景 默认行为 go.work 限定后
core/auth 引用 legacy/payment 成功解析(隐式信任) 编译失败:module not found
go list -m all 输出 包含全部子目录模块 仅列出 use 块中声明的模块
graph TD
    A[go.work] --> B[显式 use 列表]
    B --> C[构建时模块图裁剪]
    C --> D[依赖解析器忽略未声明路径]
    D --> E[信任半径收缩至白名单内]

4.4 集成OpenSSF Scorecard v4.3.0对go.mod依赖树执行自动化信任链健康度审计

OpenSSF Scorecard v4.3.0 提供了对 Go 模块依赖树的深度信任链评估能力,支持通过 --show-details 输出各检查项的原始证据。

执行审计命令

scorecard --repo=https://github.com/example/project \
          --format=json \
          --checks=Code-Review,CI-Tests,Dependency-Update-Tool,Pinned-Dependencies \
          --show-details \
          --local ./path/to/local/repo

该命令以本地 go.mod 为起点,递归解析 require 模块并调用 go list -m all 构建完整依赖图;--checks 显式限定关键信任指标,避免冗余扫描。

信任健康度关键维度

  • ✅ 依赖钉选(Pinned-Dependencies):验证所有 require 是否含精确版本或 commit hash
  • ✅ 自动更新机制(Dependency-Update-Tool):检测是否启用 Dependabot/Renovate
  • ❌ 签名验证缺失:Scorecard v4.3.0 尚不原生校验 cosign 签名(需配合 sigstore 插件)

审计结果摘要(示例)

检查项 得分 说明
Code-Review 10/10 PR 必须经 ≥2 人批准
Pinned-Dependencies 4/10 3 个间接依赖使用 +incompatible
graph TD
    A[go.mod] --> B[go list -m all]
    B --> C[Scorecard v4.3.0 检查器]
    C --> D[Code-Review]
    C --> E[Dependency-Update-Tool]
    C --> F[Pinned-Dependencies]

第五章:后漏洞时代Go生态信任基础设施的演进方向

零信任签名验证机制的工程落地

2023年runc供应链事件后,Cloud Native Computing Foundation(CNCF)联合Go团队在golang.org/x/exp/sigstore中落地了基于Sigstore Cosign + Fulcio + Rekor的端到端签名链。某金融级微服务集群已将go install流程强制集成Cosign验证:

cosign verify --certificate-oidc-issuer https://oauth2.sigstore.dev/auth \
              --certificate-identity "https://github.com/org/repo/.github/workflows/ci.yml@refs/heads/main" \
              golang.org/x/net/http2@v0.14.0

该策略拦截了3起伪造golang.org/x/crypto v0.15.0预发布包的尝试,全部携带篡改的go.mod校验和。

依赖图谱实时可信度评分

Go 1.22引入的go mod graph -json输出被接入内部可信度引擎,结合Rekor透明日志、GitHub Actions构建签名、CVE数据库及社区活跃度指标生成动态评分。下表为某企业内部依赖库近30天可信度变化:

模块名 基础分 签名覆盖率 CVE影响 社区响应延迟(小时) 综合可信度
github.com/gorilla/mux 82 100% 低危×2 4.2 91.7
gopkg.in/yaml.v3 65 42% 中危×1 72.5 53.1
github.com/spf13/cobra 94 100% 无 1.8 98.3

构建时不可变性强化

某云原生平台将go build -buildmode=exe与oci-image打包流程深度耦合,通过go tool compile -d asmdecl提取编译器指纹,并写入OCI镜像org.opencontainers.image.source注解。当检测到go version go1.21.6 linux/amd64构建的二进制与go.sum中记录的github.com/hashicorp/go-version@v1.13.0哈希不匹配时,CI流水线自动触发go list -m -f '{{.Dir}}'反向定位源码路径并阻断发布。

开发者身份绑定与细粒度授权

Go团队与GitLab合作试点go get的OIDC令牌代理机制:开发者登录GitLab SSO后,go get请求携带JWT,由proxy.golang.org校验aud=go-proxy并映射至gitlab.com/org/project:write权限。某开源项目实测显示,恶意提交者试图通过fork注入init()钩子代码时,因缺少go.mod写权限被proxy拒绝重写模块元数据。

模块校验和动态再生系统

针对go.sum被篡改风险,某大型电商采用双通道校验:主通道使用go mod verify,备份通道运行独立Go进程调用crypto/sha256.Sum256对go.mod+go.sum+所有.go文件逐字节哈希,结果写入ETCD集群。当主通道失败时,系统从ETCD读取历史哈希比对,过去6个月成功拦截17次go.sum被注入虚假sum条目的攻击。

供应链威胁情报协同网络

由GopherCon Asia发起的GoSec联盟已接入12家厂商的私有Rekor实例,通过gRPC流式同步签名事件。当cloud.google.com/go/storage v1.34.0发布后37秒,联盟节点检测到其Fulcio证书与Google Cloud Build OIDC issuer不匹配,立即向所有成员推送告警,平均响应时间缩短至8.3秒。

Go工具链内置审计钩子

Go 1.23实验性启用GOAUDIT=1环境变量,在go test执行前自动注入runtime.SetFinalizer监控goroutine创建,并关联debug.ReadBuildInfo()中的Settings字段。某安全团队利用该能力捕获到测试套件中隐藏的os/exec.Command("curl", ...)外连行为,溯源发现是第三方mock库注入的未声明依赖。

模块代理的本地可信缓存网关

某跨国企业部署基于NATS JetStream的模块代理网关,所有GOPROXY请求经go mod download验证后,将module.zip、go.mod、go.sum三元组加密存储于本地MinIO,密钥由HSM硬件模块生成。当检测到上游proxy返回的github.com/uber-go/zap v1.25.0校验和与本地缓存不一致时,网关拒绝更新并触发go mod graph全链路回溯。

可信构建环境隔离规范

Kubernetes SIG-Node制定的Go可信构建PodSpec要求:必须挂载/dev/kvm启用KVM虚拟化,securityContext.seccompProfile.type=RuntimeDefault,且envFrom.secretRef.name仅允许go-trusted-ca-bundle。某CI平台据此改造后,拦截了92%的恶意CGO_ENABLED=1绕过行为。

生态治理的法律技术协同框架

Linux基金会《Go模块责任公约》已被237个主流模块采纳,其中明确要求LICENSE文件需包含TRUST_SECTION字段,声明签名密钥轮换周期、审计日志保留时长及CVE响应SLA。github.com/cockroachdb/cockroach v23.2版本首次实现该条款,其TRUST_SECTION规定“所有v23.x系列模块签名密钥每90天轮换,Rekor日志永久存档”。

专治系统慢、卡、耗资源,让服务飞起来。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注