Posted in

Go第三方包安全审计清单:从CVE扫描到go list -deps -json,构建可信供应链的6道防火墙

第一章:Go第三方包安全审计的底层逻辑与威胁全景

Go生态高度依赖模块化协作,go.mod 文件中声明的每一个 require 语句都可能引入潜在风险。安全审计并非仅关注已知CVE,而需穿透模块加载机制、版本解析规则与构建时依赖图生成逻辑,理解Go工具链如何实际解析和锁定依赖——例如 replace 指令可绕过校验覆盖远程模块,indirect 标记常掩盖传递性高危依赖。

Go模块信任边界的脆弱性

Go不强制签名验证,模块校验完全依赖 go.sum 中的哈希快照。一旦首次拉取被污染(如MITM劫持或镜像源篡改),恶意代码即可持久化进入本地缓存。执行以下命令可检测校验失效:

go mod verify  # 验证当前模块所有依赖哈希是否匹配 go.sum

若输出 main module is not in the build list 或哈希不一致,则表明依赖完整性已被破坏。

常见威胁类型与表现特征

  • 供应链投毒:攻击者劫持废弃包名、发布恶意同名新版本(如 github.com/user/logv1.2.4-malicious
  • 间接依赖盲区go list -m all | grep -E "k8s|etcd|grpc" 可快速枚举高风险间接依赖
  • 伪版本滥用v0.0.0-20230101000000-abcdef123456 类伪版本绕过语义化版本约束,需人工审查其提交哈希真实性

审计核心动作清单

动作 命令示例 关键说明
生成最小依赖图 go list -f '{{.ImportPath}}: {{join .Deps "\n "}}' ./... \| head -20 排除测试文件,聚焦主模块直接依赖
检查未使用依赖 go mod graph \| awk '{print $1}' \| sort \| uniq -d 识别重复引入或孤儿模块
提取所有校验哈希 awk '/^github/ {print $1, $2}' go.sum 结合 deps.dev API 批量查询已知漏洞

真正的安全水位线取决于对 GOPROXYGOSUMDBGONOSUMDB 环境变量协同作用的理解——它们共同定义了模块获取、校验与豁免的决策边界。

第二章:CVE漏洞扫描与SBOM生成的工程化实践

2.1 基于ghsa、nvd和osv.dev的多源CVE数据聚合与去重

为构建高置信度漏洞知识图谱,需融合 GitHub Security Advisory(GHSA)、NVD 和 OSV.dev 三源数据。各源覆盖维度互补:GHSA 强于开源项目实时披露,NVD 提供标准化CVSS评分与CPE匹配,OSV.dev 以可重现的语义版本范围见长。

数据同步机制

采用增量轮询 + ETag缓存校验,避免全量拉取。OSV.dev 支持 ?page=1&per_page=1000 分页;NVD 提供 JSON 2.0 feed 的 lastModified 时间戳;GHSA 则通过 GraphQL API 按 publishedSince 查询。

去重核心策略

def dedupe_key(vuln):
    # 统一归一化为 (aliases[0], vendor, package, version_range)
    return (
        sorted(vuln.get("aliases", []))[0] if vuln.get("aliases") else vuln.get("id"),
        vuln.get("affected", [{}])[0].get("package", {}).get("ecosystem", ""),
        vuln.get("affected", [{}])[0].get("package", {}).get("name", ""),
        tuple(sorted(vuln.get("versions", [])))  # 简化版版本集合指纹
    )

该函数将跨源同一漏洞映射为唯一键:优先使用 CVE/GHSA/OSV ID 别名交集,辅以生态上下文消歧。版本范围暂用离散集合近似(生产环境应升级为语义区间合并)。

数据源 更新频率 标准化程度 典型别名字段
GHSA 实时 identifiers[].value
NVD 每日 cve.CVE_data_meta.ID
OSV.dev 实时 id, aliases[]
graph TD
    A[GHSA API] --> C[统一Schema]
    B[NVD JSON 2.0] --> C
    D[OSV.dev REST] --> C
    C --> E[Alias-First Dedupe]
    E --> F[Consensus CVSS & CPE]

2.2 使用trivy-go和govulncheck实现CI嵌入式静态漏洞扫描

Go 生态的漏洞检测正从依赖扫描向源码级深度分析演进。trivy-go 提供轻量 CLI 接口,直接解析 Go module graph;govulncheck 则基于官方 golang.org/x/vuln 数据库,执行 AST 级调用链追踪。

集成到 GitHub Actions 的最小工作流

- name: Run govulncheck
  run: |
    go install golang.org/x/vuln/cmd/govulncheck@latest
    govulncheck -json ./... > vulns.json

该命令递归检查所有包,-json 输出结构化结果供后续解析;./... 支持模块内子包通配,避免遗漏内部组件。

工具能力对比

工具 扫描粒度 数据源 CI 友好性
trivy-go go.mod Trivy DB + OSV ⭐⭐⭐⭐
govulncheck AST + callgraph Go Vulnerability Database ⭐⭐⭐

检测流程示意

graph TD
  A[go list -deps] --> B[提取 import path]
  B --> C[匹配 CVE/GO-XXXX]
  C --> D[构建调用路径]
  D --> E[输出可修复建议]

2.3 从go.mod解析构建SBOM(SPDX/Syft格式)并验证完整性签名

Syft 可直接从 go.mod 提取依赖树,生成标准化 SBOM:

syft -o spdx-json ./ --file sbom.spdx.json

此命令以当前目录为根,自动识别 go.mod,解析 require 模块及版本,并递归解析 replaceexclude 规则;-o spdx-json 指定输出符合 SPDX 2.3 规范的 JSON 格式。

验证签名完整性

使用 cosign 对 SBOM 文件签名并校验:

  • 签名:cosign sign-blob --key cosign.key sbom.spdx.json
  • 校验:cosign verify-blob --key cosign.pub --signature sbom.spdx.json.sig sbom.spdx.json

关键字段映射表

go.mod 字段 SBOM 属性 说明
require PackageVersion 主依赖版本
replace ExternalRef 指向本地或 fork 路径
// indirect RelationshipType 标记为 DYNAMIC_LINK
graph TD
  A[go.mod] --> B{Syft 解析}
  B --> C[SPDX Package]
  C --> D[cosign 签名]
  D --> E[Verify-blob 校验]

2.4 CVE严重性分级策略:CVSS v3.1向Go生态语义化映射(如RCE > SSRF > DoS)

Go安全工具链(如govulncheckgosec)需将CVSS v3.1的量化评分转化为开发者可直觉理解的语义优先级,而非仅依赖Base Score阈值。

映射逻辑核心原则

  • RCE(远程代码执行)→ Critical(CVSS ≥ 9.0,且AV:N+AC:L+PR:N+UI:N
  • SSRF/权限提升 → High(7.0–8.9,含网络向量但需部分交互)
  • 网络型DoS → Medium(即使CVSS=7.5,若C/I/A=NONE则降级)

Go模块中的语义化判定示例

// vuln/ranking.go
func RankByCVSS(cvss *cvss.MetricV31) Severity {
    switch {
    case cvss.BaseScore >= 9.0 && 
         cvss.AttackVector == cvss.Network && 
         cvss.PrivilegesRequired == cvss.None:
        return Critical // RCE典型组合
    case cvss.BaseScore >= 7.0 && cvss.Scope == cvss.Changed:
        return High // 跨边界影响(如SSRF触发服务端请求)
    default:
        return Medium // 兜底,排除无机密性/完整性影响的DoS
    }
}

该函数摒弃纯数值截断,结合AttackVectorPrivilegesRequiredScope三元组进行上下文感知降级;例如CVE-2023-24538(Go net/http DoS)因C/I/A=NONE被标为Medium,避免噪声告警。

CVSS向Go生态Severity映射表

CVSS Base Score Attack Vector Privileges Required Scope Go Severity
9.8 Network None Unchanged Critical
7.5 Network None Changed High
7.5 Network None Unchanged Medium
graph TD
    A[CVSS v3.1 Vector] --> B{AV==N?}
    B -->|Yes| C{PR==None ∧ Scope==Changed?}
    B -->|No| D[Medium]
    C -->|Yes| E[High]
    C -->|No| F{BaseScore ≥ 9.0?}
    F -->|Yes| G[Critical]
    F -->|No| D

2.5 实战:在GitHub Actions中自动化触发CVE扫描+阻断高危PR合并

核心流程设计

# .github/workflows/cve-scan-block.yml
on:
  pull_request:
    types: [opened, synchronize, reopened]
    branches: [main]

jobs:
  cve-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run Trivy scan
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'fs'
          ignore-unfixed: true
          format: 'table'
          exit-code: '1'  # 遇高危漏洞即失败
          severity: 'CRITICAL,HIGH'

此配置在 PR 提交时自动触发 Trivy 文件系统扫描;exit-code: '1' 强制工作流失败,阻止 GitHub 合并按钮激活;severity 限定仅阻断 CRITICAL/HIGH 级漏洞,避免误伤。

阻断机制生效逻辑

  • GitHub Actions 失败 → PR Checks 显示 ❌ cve-scan
  • 仓库设置 Require status checks to pass before merging
  • 未修复漏洞前,Merge 按钮灰显且不可点击

扫描结果示例(关键字段)

TYPE VULNERABILITY ID SEVERITY PACKAGE FIXED IN
os-pkgs CVE-2023-45802 HIGH libssl1.1 1.1.1n-1+deb11u5
graph TD
  A[PR opened] --> B[Trigger GitHub Action]
  B --> C[Trivy FS scan]
  C --> D{Critical/High found?}
  D -- Yes --> E[Job fails → Check ❌]
  D -- No --> F[Check ✅ → Merge enabled]

第三章:依赖图谱深度分析与可信供应链建模

3.1 go list -deps -json输出结构解析与依赖树反序列化实践

go list -deps -json 是 Go 模块依赖分析的核心命令,其输出为扁平化的 JSON 对象流(每行一个 JSON 对象),非标准数组。

核心字段语义

  • ImportPath: 包的唯一标识路径(如 "fmt""rsc.io/quote/v3"
  • Deps: 字符串切片,仅含直接依赖的 ImportPath
  • Module: 若非主模块,包含 PathVersionSum 等字段

反序列化关键点

需逐行解码(json.Decoder.Decode),避免 json.Unmarshal([]byte) 尝试解析多对象流导致失败:

dec := json.NewDecoder(os.Stdin)
for {
    var pkg struct {
        ImportPath string   `json:"ImportPath"`
        Deps       []string `json:"Deps"`
        Module     *struct {
            Path    string `json:"Path"`
            Version string `json:"Version"`
        } `json:"Module"`
    }
    if err := dec.Decode(&pkg); err == io.EOF {
        break
    } else if err != nil {
        log.Fatal(err) // 处理解析错误(如缺失字段)
    }
    // 构建依赖图节点
}

逻辑说明:json.Decoder 支持流式解析;Module 为指针类型,可安全判空;Deps 不含递归嵌套,需后处理构建有向图。

依赖关系拓扑示意

graph TD
    A["main.go"] --> B["fmt"]
    A --> C["github.com/gorilla/mux"]
    C --> D["net/http"]
    D --> E["io"]
字段 是否必填 示例值 说明
ImportPath "encoding/json" 包导入路径
Deps 否(空切片合法) ["unsafe","reflect"] 仅直接依赖,不含 transitive

3.2 识别transitive依赖中的“幽灵包”(无go.sum记录但实际加载的间接依赖)

Go 模块构建时,某些 transitive 依赖可能绕过 go.sum 校验——它们未被显式记录,却在编译期被 go list -depsgo build -x 实际加载。

什么是“幽灵包”?

  • 未出现在 go.sum
  • 不在 go.modrequire 块中直接声明
  • 却被某一级依赖的 import 语句触发加载

复现与检测

# 查看真实加载的包(含幽灵包)
go list -f '{{.ImportPath}} {{.DepOnly}}' -deps ./... | grep "true"

该命令输出所有仅作为依赖(非直接 import)的包路径。DepOnly=true 表明其为纯间接依赖,若对应模块无 go.sum 条目,则属幽灵包。

检测手段 是否捕获幽灵包 说明
go mod graph 仅展示模块级依赖关系
go list -deps 显示包级实际加载链
go mod verify 仅校验 go.sum 已存条目
graph TD
    A[main.go import pkgA] --> B[pkgA imports pkgB]
    B --> C[pkgB imports pkgC]
    C --> D[pkgC not in go.sum]
    D --> E[幽灵包:运行时存在,校验时隐身]

3.3 构建最小可信依赖图:基于module path签名与cosign验证链追溯

可信依赖图的构建始于模块路径(module path)的唯一性锚定。每个 Go 模块在 go.mod 中声明的路径(如 github.com/example/lib)不仅是导入标识,更是签名绑定的不可变上下文。

cosign 验证链提取

使用 cosign verify-blob 提取模块归档的签名链:

cosign verify-blob \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp "https://github.com/example/lib/.github/workflows/release.yml@refs/heads/main" \
  --cert lib-v1.2.0.zip.cert \
  lib-v1.2.0.zip
  • --certificate-oidc-issuer 指定信任的 OIDC 发行方;
  • --certificate-identity-regexp 精确匹配 CI 身份正则,防止身份漂移;
  • 输出含 subjectissuerchain 字段,构成可追溯的证书链。

最小依赖图生成逻辑

依赖图节点由 (module_path@version, signature_digest) 二元组唯一标识,边由 require 指令隐式定义。仅保留经 cosign 验证通过且签名链完整(≥2级 CA)的节点。

节点属性 示例值
module_path golang.org/x/crypto
version v0.25.0
signature_chain sig → intermediate → root (Sigstore)
graph TD
  A[lib-v1.2.0.zip] -->|cosign verify-blob| B[Cert: GH Actions]
  B --> C[Intermediate CA]
  C --> D[Root CA: Sigstore Fulcio]

第四章:go.sum校验强化与不可变构建保障机制

4.1 go.sum哈希算法兼容性分析:sumdb校验失败的8类典型根因诊断

数据同步机制

sum.golang.org 与本地 go.sum 的哈希比对依赖确定性哈希算法(h1: 前缀为 SHA256)。当模块发布后被重写(如 tag force-push)、或使用非标准 checksum 生成方式时,校验即失败。

典型根因归类

  • 模块作者修改已发布 tag 的 commit 内容
  • 使用 go mod edit -replace 后未更新 go.sum
  • 交叉构建环境导致 GOOS/GOARCH 相关文件哈希不一致
  • //go:build 条件编译引入路径差异
  • Go 版本升级引发 go.sum 格式变更(如 Go 1.18+ 新增 h1: 校验项)
  • 代理缓存污染(如 Athens、goproxy.cn 未及时刷新)
  • GOPROXY=direct 下直连 sumdb 时 DNS 或 TLS 中断
  • GOSUMDB=offGOSUMDB=sum.golang.org 混用导致状态不一致

校验失败诊断流程

graph TD
    A[go get 失败] --> B{检查 go.sum 是否含对应行}
    B -->|否| C[运行 go mod download -json]
    B -->|是| D[对比 sumdb 返回值与本地 h1:...]
    D --> E[不一致 → 检查模块 Git 历史是否篡改]

实例验证

# 获取远程校验和(Go 1.21+)
go list -m -json -versions example.com/pkg@v1.2.3 | \
  jq '.Version, .Sum'  # 输出形如 "h1:abc123...="

该命令调用 sum.golang.org/lookup 接口,返回经签名的 h1: 哈希;若本地 go.sum 中对应行值不匹配,则触发 checksum mismatch 错误。-json 输出确保结构化解析,避免正则误匹配。

4.2 使用goreleaser+cosign+notary v2实现二进制与module checksum双签验

现代Go生态需同时保障发布制品(binary)依赖完整性(go.sum) 的可信链。goreleaser 负责构建与打包,cosign 对二进制签名并上传至 OCI registry,而 notary v2(通过 oras CLI 或 notation 工具)为 go.sum 文件生成独立签名并存证。

双签流程协同机制

# goreleaser 构建后注入 cosign 签名
- name: sign-binaries
  script:
    - cosign sign --key $COSIGN_PRIVATE_KEY ./dist/myapp_{{.Version}}_linux_amd64

此步骤对每个平台二进制执行密钥签名,--key 指向硬件/云HSM托管的私钥;签名以透明日志(TUF/Sigstore Rekor)存证,供下游 cosign verify 校验。

module checksum 签名策略

签名对象 工具 存储位置 验证方式
go.sum notation OCI registry tag go mod download -v + notation verify
graph TD
  A[goreleaser build] --> B[cosign sign binaries]
  A --> C[generate go.sum]
  C --> D[notation sign go.sum]
  B & D --> E[OCI registry with attestations]

双签验证时,CI流水线先 cosign verify 二进制,再 notation verify 对应 go.sum,确保运行时与构建时依赖图完全一致。

4.3 在Bazel/Earthly中复现go build –mod=readonly的不可变构建沙箱

go build --mod=readonly 强制禁止修改 go.modgo.sum,是构建可重现性的关键防线。在 Bazel 和 Earthly 中需通过沙箱隔离与声明式依赖约束实现等效语义。

Bazel:通过 go_repositoryimmutable 标记

# WORKSPACE
go_repository(
    name = "com_github_pkg_errors",
    importpath = "github.com/pkg/errors",
    sum = "h1:1uqIyYcG68gO0wvHdUJm2K5z7Ck9P7ZQXjxYJ1VzF1E=",
    version = "v0.9.1",
    # Bazel 默认拒绝写入模块文件,天然符合 --mod=readonly 语义
)

go_repository 在分析阶段即锁定版本与校验和,所有依赖解析离线完成;构建时不调用 go mod download,无副作用。

Earthly:显式禁用模块写入

build:
    FROM golang:1.22-alpine
    RUN go env -w GONOSUMDB=*  # 避免动态校验干扰
    COPY go.mod go.sum .
    RUN go mod download && go mod verify  # 仅验证,不修改
    COPY . .
    RUN CGO_ENABLED=0 go build -mod=readonly -o /bin/app .
工具 模块写入控制方式 是否默认满足 --mod=readonly
Bazel 声明式 go_repository ✅ 是(无隐式 go mod 调用)
Earthly 构建阶段显式 RUN ... -mod=readonly ⚠️ 需手动指定,否则可能触发 go mod tidy
graph TD
    A[源码 + go.mod/go.sum] --> B{Bazel}
    A --> C{Earthly}
    B --> D[静态解析依赖<br>沙箱内无 go mod 写入]
    C --> E[执行 RUN go build -mod=readonly]
    E --> F[失败若检测到潜在修改]

4.4 实战:通过go mod verify hook拦截篡改的vendor目录与伪造的replace指令

Go 1.21+ 支持 go mod verify 钩子机制,可在 go build 前自动校验依赖完整性。

校验流程概览

graph TD
    A[go build] --> B[触发 go mod verify]
    B --> C{检查 vendor/ hash}
    C -->|不匹配| D[拒绝构建并报错]
    C -->|匹配| E{验证 replace 指令签名}
    E -->|未签名/伪造| F[终止执行]

配置 verify hook

# 在 go.work 或 GOPATH 下配置
GOVERIFYSUMS=strict \
GOSUMDB=sum.golang.org \
go mod verify
  • GOVERIFYSUMS=strict:强制校验所有模块哈希,禁用本地缓存绕过;
  • GOSUMDB 指定权威校验源,防止中间人篡改 sumdb 响应。

关键防护点对比

风险项 默认行为 verify hook 启用后
vendor 目录被替换 无感知编译通过 哈希不匹配 → 构建失败
replace ./local 允许任意路径映射 仅接受 GOSUMDB 签名白名单路径

启用后,任何未经 sum.golang.org 签名的 replace 指令或 vendor/ 内容篡改均被实时拦截。

第五章:构建可信Go供应链的终局思考与演进路径

从一次真实漏洞响应看供应链断裂点

2023年10月,某金融级API网关项目因间接依赖 golang.org/x/text@v0.14.0 中的 unicode/norm 模块缓冲区越界问题触发紧急升级。团队耗时17小时定位到根源——并非直接引入该包,而是通过 github.com/gorilla/mux@v1.8.0github.com/gorilla/securecookie@v1.1.1golang.org/x/text@v0.3.7 的三级传递链引入了已知CVE-2023-45858。此案例暴露了 go list -m all 无法自动识别语义化版本别名(如 v0.3.7 实际对应 v0.14.0 的retract后版本)的根本缺陷。

构建可验证的模块签名流水线

某云原生平台采用 Cosign + Fulcio + Rekor 实现全链路签名:

  • CI阶段对每个 go mod download -json 输出的模块哈希生成SLSA Level 3证明
  • 使用 cosign sign-blob --key ./signing.key module.json 签署模块元数据
  • 通过 rekor-cli store --artifact module.json --signature sig.sig 存证至透明日志
# 验证示例:检测生产环境模块是否被篡改
go run github.com/sigstore/cosign/cmd/cosign@v2.2.3 verify-blob \
  --certificate-identity-regexp "https://github\.com/org/repo/.+" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --cert rekor.crt module.json

依赖图谱的动态裁剪策略

某电商中台项目维护着包含2,387个模块的 go.sum 文件,但实际运行时仅加载其中31%的代码。通过 go tool trace 结合 pprof -http=:8080 分析10万次HTTP请求的调用栈,生成最小依赖集:

模块路径 调用频次 是否可裁剪 裁剪后体积减少
github.com/aws/aws-sdk-go-v2/config 98,241
gopkg.in/yaml.v2 0 1.2MB
github.com/mattn/go-sqlite3 0 4.7MB

构建确定性构建环境

采用 Nixpkgs 的 Go 交叉编译方案,确保 GOOS=linux GOARCH=amd64 go build 在不同开发者机器上产生完全一致的二进制哈希。关键配置片段:

{ pkgs ? import <nixpkgs> {} }:
pkgs.stdenv.mkDerivation {
  name = "my-go-app";
  src = ./.;
  buildInputs = [ pkgs.go_1_21 ];
  buildPhase = ''
    export GOCACHE=$(mktemp -d)
    export GOPATH=$(mktemp -d)
    cd $src
    go build -trimpath -ldflags="-buildid=" -o myapp .
  '';
}

开源组件健康度实时监控

集成 deps.dev API 构建内部仪表盘,对 github.com/uber-go/zap 等核心依赖实施三重告警:

  • 主版本更新滞后超90天(当前使用 v1.24.0,最新为 v1.26.0
  • 安全公告未修复率 > 15%(检测到3个中危CVE,仅修复1个)
  • 维护者响应延迟 > 72小时(最近PR平均响应时长108小时)

供应商锁定风险的渐进式解耦

某支付网关将 cloud.google.com/go/firestore 替换为抽象层 pkg/storage,通过接口定义和适配器模式实现双引擎支持:

  • Firestore适配器:firestoreClient.Get(ctx, key)storage.Read(ctx, key)
  • 自研KV引擎:redisClient.Get(ctx, key)storage.Read(ctx, key)
  • 运行时通过 STORAGE_DRIVER=redis 环境变量切换,灰度发布期间同时写入双存储并比对一致性

可信构建的硬件信任根延伸

在ARM64服务器集群中启用TPM 2.0 attestation:

  • 构建节点启动时通过 tpm2_pcrread sha256:0,7,10 获取PCR值
  • 将PCR摘要注入Cosign证明的 x509.SubjectAlternativeNames 字段
  • 验证方通过 rekor-cli get --uuid ${UUID} --format json | jq '.body.attestation.x509Certificate.subjectAlternativeNames' 校验运行时完整性

模块代理的主动防御机制

自建 goproxy.io 兼容代理服务,集成以下防护:

  • github.com/*/* 请求自动重写为 https://github.com/{owner}/{repo}/archive/refs/tags/{version}.tar.gz 直链,规避恶意fork劫持
  • 拦截含 exec.Command("sh", "-c", ...) 特征的 go.mod 替换指令
  • // indirect 标记的模块强制要求提供SLSA证明,缺失则拒绝下载

供应链攻击面的持续测绘

使用 go list -deps -f '{{.ImportPath}} {{.Module.Path}}' ./... 生成依赖图谱,结合Graphviz生成可视化拓扑:

graph LR
  A[main.go] --> B[golang.org/x/net/http2]
  A --> C[github.com/gin-gonic/gin]
  C --> D[github.com/go-playground/validator/v10]
  D --> E[golang.org/x/exp/maps]
  style E fill:#ff9999,stroke:#333

红色节点标识高风险模块(无维护者、近半年零提交、存在未修复CVE)

十年码龄,从 C++ 到 Go,经验沉淀,娓娓道来。

发表回复

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