核心价值与目标依赖锁定(lockfile)确保可重复构建;完整性签名与哈希防止被替换;审计与SBOM提高可见性与修复效率。建立“可证明”的发布链路:从拉取、构建到交付的每一步均可追溯与核验。锁定文件治理使用 `pnpm-lock.yaml`/`package-lock.json`/`yarn.lock` 固定版本与依赖树;所有构建均基于锁定文件。CI 强制校验:构建前执行一致性检查与漂移检测。pnpm install --frozen-lockfile npm ci 完整性与签名npm 完整性字段:`integrity`(sha512/sha1)在 lockfile 与 `package.json` 的 tarball 解析中使用。使用 Sigstore/cosign 对构建产物或容器进行签名与验签(如前端产物目录或镜像)。cosign sign-blob --key cosign.key dist.tar.gz > dist.sig cosign verify-blob --key cosign.pub --signature dist.sig dist.tar.gz SBOM(软件物料清单)与审计生成 SBOM(CycloneDX/SPDX)提升依赖可见性与合规审计能力。npx @cyclonedx/cyclonedx-npm --output-format json --output-file sbom.json 依赖安全审计:npm audit --json osv-scanner -r . 供应链风险治理策略只允许可信源:配置私有 registry 与镜像;锁定作用域与组织包名。禁止“latest”:始终使用明确版本;高风险依赖进入阻断名单,需安全评审后方可引入。构建隔离:在干净环境中构建(容器/沙箱);禁用 postinstall 扩权脚本。指标与验证(真实项目抽样,Chrome/Node 工具链)审计覆盖率:高危与中危项识别率 ≥ 99%。修复响应效率:高危项从发现到修复 ≤ 48h;中危 ≤ 7d。可重复构建:锁定文件漂移检测命中率 100%,构建哈希一致率 ≥ 99.9%。供应链事件拦截:非签名或哈希不匹配产物在 CI/CD 全部阻断(抽检 100%)。发布链路示例(概要)拉取:仅可访问私有 registry;校验 lockfile 与 registry 白名单。构建:`npm ci`/`pnpm install --frozen-lockfile`;生成 SBOM;禁用危险脚本。产物:打包并以 cosign 签名;输出 SRI 哈希用于前端注入。交付:CD 校验签名与 SRI;对外静态资源强制 CSP `require-sri-for script style`。测试清单锁定漂移:修改版本或 registry 源,CI 立即失败。篡改检测:替换产物文件,验签与 SRI 失败并阻断发布。审计告警:引入已知漏洞版本,审计工具命中并生成报告。应用场景多团队前端/Node 工程的统一依赖治理;对安全/合规敏感项目的“可证明交付”。title: 依赖锁定与供应链安全审计(Lockfile-签名-完整性)最佳实践categories:Web 开发前端安全keywords:依赖锁定lockfile完整性哈希签名SBOMdescription: 通过锁定文件、哈希与签名验证、来源白名单与版本治理,建立可审计的依赖供应链,降低篡改与投毒风险。author: YBBdate: 2025-11-25status: publishedhotness: 8tech_verified: truereading_time: 10 min---核心要点严格使用 `package-lock.json`、`yarn.lock` 或 `pnpm-lock.yaml`,禁止非锁定安装。对每个制品的 `integrity` 与 `resolved` 执行校验;仅允许 `https` 且域名在白名单内。版本必须符合语义化规范,禁止未锁定的范围依赖;必要时启用冻结模式。生成并签名 SBOM,构建产物与 SBOM 双向绑定以支持审计与回溯。在 CI 设审计门禁:缺失锁定、哈希不匹配、来源不合规直接失败。参数与规则注册表白名单:`https://registry.npmjs.org`、企业内镜像域名。允许算法:`sha256`;禁止弱算法;SRI 需形如 `sha256-<base64>`。版本格式:`MAJOR.MINOR.PATCH` 与可选预发布或构建元数据;禁止 `^`、`~` 等范围对运行制品。令牌权限最小化:只读拉取;发布与篡改分离权限。实现示例type Entry = { name: string; version: string; resolved: string; integrity: string } const allowRegistries = new Set<string>([ 'https://registry.npmjs.org', 'https://registry.example.com' ]) function semverValid(v: string): boolean { return /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.test(v) } function isHttpsUrl(u: string): boolean { try { const url = new URL(u) return url.protocol === 'https:' && allowRegistries.has(url.origin) } catch { return false } } function parseSri(integrity: string): { alg: 'sha256'; b64: string } | null { const m = /^sha256-([A-Za-z0-9+/=]+)$/.exec(integrity) return m ? { alg: 'sha256', b64: m[1] } : null } async function sha256Base64(buf: Uint8Array): Promise<string> { const d = await crypto.subtle.digest('SHA-256', buf) const b = Buffer.from(d) return b.toString('base64') } function validEntry(e: Entry): boolean { if (!e.name || !semverValid(e.version)) return false if (!isHttpsUrl(e.resolved)) return false return parseSri(e.integrity) !== null } async function verifyTarballIntegrity(buf: Uint8Array, integrity: string): Promise<boolean> { const sri = parseSri(integrity) if (!sri) return false const calc = await sha256Base64(buf) return calc === sri.b64 } function validateLockEntries(entries: Entry[]): { ok: boolean; errors: string[] } { const errors: string[] = [] for (const e of entries) { if (!validEntry(e)) errors.push(`invalid:${e.name}`) } return { ok: errors.length === 0, errors } } type Manifest = { name: string; version: string; entries: Entry[] } async function signManifest(manifest: Manifest, jwk: JsonWebKey, kid: string): Promise<{ kid: string; alg: string; sig: string }> { const data = Buffer.from(JSON.stringify(manifest)) const key = await crypto.subtle.importKey('jwk', jwk, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['sign']) const sig = await crypto.subtle.sign({ name: 'RSASSA-PKCS1-v1_5' }, key, data) return { kid, alg: 'RS256', sig: Buffer.from(sig).toString('base64') } } async function verifyManifest(manifest: Manifest, signed: { kid: string; alg: string; sig: string }, jwk: JsonWebKey): Promise<boolean> { if (signed.alg !== 'RS256') return false const data = Buffer.from(JSON.stringify(manifest)) const key = await crypto.subtle.importKey('jwk', jwk, { name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' }, false, ['verify']) const ok = await crypto.subtle.verify({ name: 'RSASSA-PKCS1-v1_5' }, key, Buffer.from(signed.sig, 'base64'), data) return ok } function enforceLockedInstall(hasLockfile: boolean, allowUnpinned: boolean): boolean { if (!hasLockfile) return false return allowUnpinned === false } 审计与CI门禁校验锁定文件存在且未被篡改;构建前解析并验证全部条目。下载阶段对每个制品验证 `sha256` 与来源域名;失败即停止流水线。将构建清单与 SBOM 签名并存档;产物元数据包含 SBOM 摘要与签名指纹。记录审计日志:版本、哈希、来源、签名密钥 `kid`、时间窗口与审批人。注意事项对开发依赖与运行依赖分层治理;生产制品严禁范围版本。当注册表切换或镜像故障时启用受控回退并保留审计记录。定期轮换签名密钥并保留可验证的历史链路;禁止共享私钥。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部