lirantal/npm-security-best-practices

GitHub: lirantal/npm-security-best-practices

一份精心整理的 npm 包安全最佳实践清单,帮助开发者系统性地防御供应链攻击并加固 JavaScript 项目安全。

Stars: 614 | Forks: 21

精选 npm 安全最佳实践

一份精心整理且实用的使用 npm 包的安全最佳实践清单。

Awesome npm security best practices npm Security Best Practices

**范围**: - 默认安全的 npm 包管理器命令行选项 - 防御供应链攻击的加固措施 - 确定且安全的依赖解析 - 安全漏洞扫描与包健康信号 - 适用情况下的 `pnpm` 和 `bun` 包管理器相关说明 **背景**:Shai-Hulud、Nx 及其他事件引发了人们对供应链安全攻击和受损 npm 包的日益担忧。请遵循这些围绕 npm、包维护和安全本地开发的开发者安全最佳实践,以降低安全风险。 ## 目录 **npm 安全最佳实践:** - 1 [禁用 Post-Install 脚本](#1-disable-post-install-scripts) - 1.1. [pnpm 禁用 post-install 脚本](#11-pnpm-disable-post-install-scripts) - 1.2. [Bun 禁用 post-install 脚本](#12-bun-disable-post-install-scripts) - 1.3. [运行你需要的脚本](#13-run-the-scripts-you-need) - 1.4. [pnpm 信任策略](#14-pnpm-trust-policy) - 2 [冷却期安装](#2-install-with-cooldown) - 2.1. [npm / pnpm / Bun / Yarn minimumReleaseAge 冷却期](#21-npm--pnpm--bun--yarn-minimumreleaseage-cooldown) - 2.2. [Snyk 带冷却期的自动依赖升级](#22-snyk-automated-dependency-upgrades-with-cooldown) - 2.3. [Dependabot 带冷却期的自动依赖升级](#23-dependabot-automated-dependency-upgrades-with-cooldown) - 2.4. [Renovate bot 带冷却期的自动依赖升级](#24-renovate-bot-automated-dependency-upgrades-with-cooldown) - 3 [使用安全工具加固包安装](#3-harden-package-installs-with-security-tools) - 3.1. [使用 npq 加固包安装](#31-use-npq-for-hardening-package-installs) - 3.2. [使用 Socket Firewall (sfw) 阻拦恶意包](#32-use-socket-firewall-sfw-for-blocking-malicious-packages) - 4 [防止 npm lockfile 注入](#4-prevent-npm-lockfile-injection) - 5 [使用 npm ci](#5-use-npm-ci) - 6 [避免盲目升级 npm 包](#6-avoid-blind-npm-package-upgrades) **安全本地开发最佳实践:** - 7 [.env 文件中不应存放明文密钥](#7-no-plaintext-secrets-in-env-files) - 8 [在 Dev Containers 中工作](#8-work-in-dev-containers) **npm 维护者安全最佳实践:** - 9 [为 npm 账号启用双重身份验证 (2FA)](#9-enable-2fa-for-npm-accounts) - 10 [发布时附带来源证明](#10-publish-with-provenance-attestations) - 11 [使用 OIDC 发布](#11-publish-with-oidc) - 12 [精简包依赖树](#12-reduce-your-package-dependency-tree) **npm 包健康最佳实践:** - 13 [查阅 Snyk 安全数据库了解包健康状况](#13-consult-the-snyk-security-database-for-package-health) - 14 [不要盲目信任官方 npmjs.org registry](#14-do-not-trust-the-official-npmjsorg-registry) ## 1. 禁用 Post-Install 脚本 近期的攻击(如 Shai-Hulud[^1]、Nx[^2])以及长期的攻击(如 event-stream[^3])都利用了 npm 的 `postinstall` 脚本,在包安装期间于开发者的机器上执行任意代码,以窃取敏感数据、触发蠕虫式传播或执行其他恶意活动。 通过禁用 post-install 脚本,你可以阻止在安装过程中执行具有潜在危害的代码,从而减轻此类攻击的风险。 ### 1.1. pnpm 禁用 post-install 脚本 从 10.0 版本开始,[pnpm 默认禁用 postinstall 脚本](https://pnpm.io/supply-chain-security)。pnpm 提供了一个“逃生舱口(escape hatch)”来重新启用 postinstall 脚本,或者设置一个明确的允许列表,指定哪些包可以运行 postinstall 脚本。 使用 `pnpm-workspace.yaml` 来控制哪些包被允许运行构建脚本: ``` # pnpm-workspace.yaml # 仅允许特定包运行生命周期脚本 (pnpm 10+) onlyBuiltDependencies: - esbuild - fsevents - nx@21.6.4 || 21.6.5 # pin allowed versions # 静默拦截特定包并抑制警告 ignoredBuiltDependencies: - sharp ``` 为了让未经审查的构建脚本直接报错(而不仅仅是警告),请启用 `strictDepBuilds`(pnpm 10.3+): ``` strictDepBuilds: true ``` 当设置 `strictDepBuilds: true` 时,如果有任何依赖试图运行未被明确允许的生命周期脚本,`pnpm install` 将以非零状态码退出,从而将静默警告转变为阻断 CI 的失败。 ### 1.2. Bun 禁用 post-install 脚本 [Bun 默认禁用 postinstall 脚本](https://bun.com/docs/install/lifecycle),并维护着一份自己的内部允许列表,记录哪些包可以运行 postinstall 脚本。Bun 允许通过 `package.json` 中的 `trustedDependencies` 字段来提供一个“逃生舱口”,为特定的[受信包](https://bun.com/docs/install/lifecycle#trusteddependencies)启用 postinstall 脚本。 ### 1.3 运行你需要的脚本 有些安装脚本的存在是有原因的。如果你需要运行它们,请以可审计的方式进行,避免 npm 过于信任 `package.json` 中的包名。 使用 https://www.npmjs.com/package/@lavamoat/allow-scripts 在你的依赖图中创建特定位置的脚本允许列表。 ### 1.4. pnpm 信任策略 pnpm 10.21+ 内置了 `trustPolicy` 设置,它可以检测到一个包的**发布时信任级别是否比早期版本有所降低**——例如,当一个以前通过受信发布者(OIDC/GitHub Actions)发布的包,现在在没有来源证明或签名的情况下发布时。这可能是账户被盗用或遭受供应链攻击的早期信号。 pnpm 识别的信任级别(从最强 → 最弱): 1. **Trusted Publisher(受信发布者)** – 通过配置的受信发布者(如 GitHub Actions OIDC)发布 2. **Provenance(来源证明)** – 附带 npm 来源证明发布 3. **Signatures(签名)** – 存在包注册中心签名 4. **No evidence(无证据)** – 没有任何信任信号 当启用 `trustPolicy: no-downgrade` 时,如果某个包之前发布的版本比当前要安装的版本具有更高的信任级别,安装过程将被中止。 ## 2. 冷却期安装 攻击者利用倾向于并解析为最新 semver 范围的 npm 版本控制和发布模型,通过发布包的新版本来实施攻击。通过在安装或升级到新的包版本之前实施“冷却”期,你可以降低安装那些可能很快被发现并从注册中心删除的受损包的风险。 ### 2.1. npm / pnpm / Bun / Yarn minimumReleaseAge 冷却期 通过在包管理器的配置文件中设置最小发布时间,配置 npm、pnpm、Bun 或 Yarn 延迟包的安装。 对于 npm,在 `.npmrc` 中设置 `min-release-age`(或通过 `npm config set` 设置): ``` # .npmrc min-release-age=3 ``` 或者全局设置,以便你机器上的所有项目都能受益: ``` $ npm config set min-release-age 3 ``` 对于 pnpm 10.16+,请使用 [`pnpm-workspace.yaml`](https://pnpm.io/settings#minimumreleaseageexclude): ``` minimumReleaseAge: 20160 # 2 weeks (in minutes) # 允许 @types/react 和 typescript 即时升级 minimumReleaseAgeExclude: - '@types/react' - typescript ``` 对于 Bun 1.3+,请使用 [`bunfig.toml`](https://github.com/oven-sh/bun/issues/22679#issuecomment-3371327793): ``` # bunfig.toml [install] # 仅安装至少发布 3 天的包版本 minimumReleaseAge = 259200 # seconds - in #23162 it'll allow "3d" too # 这些包将绕过 3 天最低发布时间要求 minimumReleaseAgeExcludes = ["@types/bun", "typescript"] ``` 对于 Yarn 4.10+,请使用 [`.yarnrc.yml`](https://yarnpkg.com/configuration/yarnrc#npmMinimalAgeGate): ``` # .yarnrc.yml # 仅考虑至少发布 3 天的 npm 包版本 npmMinimalAgeGate: "3d" # 这些包将绕过发布时间限制(包描述符或 glob 模式) npmPreapprovedPackages: - "@types/react" - "typescript" ``` 这些配置会阻止包管理器安装那些发布时间少于指定时间的包版本。 ### 2.2. Snyk 带冷却期的自动依赖升级 [Snyk 自动为依赖升级的 Pull Requests 包含内置的冷却期](https://docs.snyk.io/scan-with-snyk/pull-requests/snyk-pull-or-merge-requests/upgrade-dependencies-with-automatic-prs-upgrade-prs/upgrade-open-source-dependencies-with-automatic-prs#automatic-dependency-upgrade-prs)。Snyk 不建议升级到发布不到 21 天的版本,以避免: - 引入功能性错误随后被取消发布的版本 - 从被盗用的账户发布的版本,这些账户的控制权已被恶意行为者夺取 ### 2.3. Dependabot 带冷却期的自动依赖升级 Dependabot 提供了一个 [`cooldown`](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference#cooldown-) 配置选项,用于设置依赖的特定版本在发布多少天后才会被更新: ### 2.4. Renovate bot 带冷却期的自动依赖升级 Renovate bot 提供了一个 [`minimumReleaseAge`](https://docs.renovatebot.com/configuration-options/#minimumreleaseage) 配置选项,用于设置在为其创建 Pull Request 之前,每个包版本必须达到的最小发布时间: ## 3. 使用安全工具加固包安装 你如何知道一个 npm 包是否可以安全安装?也许它昨天才刚刚发布?也许你在包名中不小心打错了字,结果找到了一个名称相似的恶意包?也许这个包存在已知漏洞或恶意的 post-install 脚本?恶意包可能会在安装过程中执行任意代码、窃取敏感数据,或在不知情的情况下将漏洞引入你的系统。 安装新的临时 npm 包可能会使你的系统面临供应链攻击。许多攻击都攻破了受信任且受欢迎的 npm 包,利用了 typosquatting(误植域名/包名抢注),或者在安装过程中执行的 pre/post-install 脚本中引入恶意代码。 ### 3.1. 使用 npq 加固包安装 #### npq 验证的内容 npq 使用“marshalls”(专门的安全验证器)执行全面的安全审计,它们会检查: - **漏洞扫描**:查阅 Snyk 的数据库以查找已知的 CVE 漏洞 - **包年龄分析**:标记发布不到 22 天的包(新包检测) - **Typosquatting 检测**:识别名称与流行包相似的包 - **注册中心签名验证**:使用已发布的密钥验证 npm 注册中心签名 - **来源证明**:验证包构建来源的元数据 - **Pre/post-install 脚本**:对具有潜在恶意的安装脚本发出警告 - **包健康指标**:检查 README、LICENSE、仓库 URL 和下载指标 - **版本成熟度**:标记发布不到 7 天的包版本 - **二进制文件引入**:当添加新的命令行二进制文件时发出警告 - **弃用状态**:对已弃用的包发出警报 - **维护者域名验证**:检查维护者电子邮件中的过期域名 #### pnpm 和 Bun 兼容性 npq 可通过环境变量与不同的包管理器配合使用: ``` # 配合 pnpm 使用 NPQ_PKG_MGR=pnpm npq install fastify # 配合 Bun 使用 NPQ_PKG_MGR=bun npq install fastify # 设置永久别名 alias pnpm="NPQ_PKG_MGR=pnpm npq-hero" ``` #### 高级用法选项 运行安全检查而不安装包: ``` $ npq install express --dry-run ``` 在需要时禁用特定的安全 marshalls: ``` $ MARSHALL_DISABLE_SNYK=1 npq install express ``` ### 3.2. 使用 Socket Firewall (sfw) 阻拦恶意包 #### Socket Firewall 检查的内容 Socket Firewall 使用 Socket 的威胁情报对包进行深度分析,检查内容包括: - **恶意代码检测**:识别包含已知恶意软件或混淆代码的包 - **安装脚本风险**:标记具有可疑 pre/post-install 脚本的包 - **Typosquatting 检测**:捕获名称与流行包相似的包 - **依赖混淆**:检测潜在的依赖混淆攻击 - **已知漏洞**:交叉比对 CVE 数据库以查找已披露的漏洞 - **Protestware 和环境变量访问**:警告访问环境变量或表现出 protestware(抗议软件)行为的包 - **网络和文件系统访问**:突出显示执行意外网络或磁盘操作的包 #### 与 npq 的比较 `npq` 和 `sfw` 都会拦截包的安装并提供安全警告,但它们的方法有所不同: | | npq | sfw (Socket Firewall) | |---|---|---| | **分析方法** | 通过可配置的“marshalls”进行安装前检查 | 通过 Socket 平台进行实时深度包分析 | | **数据来源** | Snyk CVE 数据库,npm 注册中心元数据 | Socket 专有的威胁情报和静态分析 | | **交互性** | 安装前的交互式提示 | 阻止安装并提示被标记的包 | | **包管理器支持** | npm、pnpm、Bun(通过环境变量) | npm、yarn、pnpm、pip、uv、cargo | | **开源** | 是 | 客户端开源;分析平台为专有 | ## 4. 防止 npm lockfile 注入 2019 年 9 月,Liran Tal 公布了一项关于开发人员工作流中包 lockfile 固有安全风险的安全研究。JavaScript 包管理器 Yarn 和 npm 都被发现容易受到 lockfile 注入攻击。 当恶意行为者能够通过 Pull Request 等机制提供源代码更改时,这种安全威胁就会发生。如果他们更新了 `package-lock.json` 或 `yarn.lock` 等 lockfile 以包含新的 npm 包依赖,或者修改了现有包的源 URL,那么任何包安装命令的调用都会拉取这些恶意代码。 此外,JavaScript 包管理器允许用户从非常规来源安装包,例如 GitHub gists 或直接从源代码库安装。攻击者可以更新 lockfile 以指定他们控制的新源位置(在 `resolved` 键中),并相应地设置 SHA-512 完整性哈希值以逃避检测。 ### Lockfile-lint 验证选项 `lockfile-lint` CLI 提供全面的验证以确保 lockfile 的完整性: - **主机验证**:将包在受信任的注册中心主机(npm、yarn、verdaccio) - **HTTPS 强制**:确保所有包源使用安全的 HTTPS 协议 - **协议验证**:控制允许的 URI 协议(https:、git+https:、git+ssh:) - **包名验证**:验证解析后的 URL 是否与声明的包名匹配 - **完整性验证**:确保完整性哈希使用安全的 SHA-512 算法 ### CI/CD 集成 将 lockfile-lint 集成到你的开发工作流中,例如在 `package.json` 中添加以下 `lint:lockfile` 脚本,并在每次安装前运行它: ``` { "scripts": { "lint:lockfile": "lockfile-lint --path package-lock.json --type npm --allowed-hosts npm --validate-https", "preinstall": "npm run lint:lockfile" } } ``` ### pnpm lockfile 注入安全性 pnpm 不易受到与 npm 和 yarn 相同的 lockfile 注入漏洞的攻击,因为: - 它不维护可能被恶意修改的 tarball 源 - 它不会安装 lockfile 中列出但未在 `package.json` 中声明的包 - `pnpm-lock.yaml` 格式对注入攻击具有更强的抵抗力 ### pnpm blockExoticSubdeps 即使 lockfile 是干净的,传递依赖也可能从任意的 Git 仓库或原始 tarball URL 拉取代码——这些来源对于典型的注册中心安全扫描是不透明的。pnpm 10.26+ 引入了 `blockExoticSubdeps` 来防止这种情况。 ### Bun lockfile linting Bun 使用自己的 lockfile 格式——即 `bun.lock`(基于文本,自 v1.2 起默认)或 `bun.lockb`(二进制格式)。目前,`lockfile-lint` 不支持 Bun 的 lockfile 格式。 ## 5. 使用 npm ci 像 npm 和 yarn 这样的包管理器,会通过安装与 lockfile 中记录的版本来弥补 `package.json` 和 lockfile 之间的不一致性。这种行为对于构建和生产环境来说是非常危险的,因为它们可能会拉取到意外的包版本,使得 lockfile 确定性的所有优势化为乌有。开发者在本地开发工作流中也应倾向于使用确定性的包解析。 ### Yarn、Bun、Deno 和 pnpm 包管理器的确定性安装 不同的包管理器提供了特定的命令来强制遵守 lockfile: **yarn**:验证 lockfile(和本地缓存)未发生改变: ``` $ yarn install --immutable --immutable-cache ``` **pnpm**:使用冻结 lockfile 安装: ``` $ pnpm install --frozen-lockfile ``` **Bun**:使用冻结 lockfile 模式: ``` $ bun install --frozen-lockfile ``` **Deno**:使用冻结安装: ``` $ deno install --frozen ``` ### Lockfile 管理最佳实践 确保在整个开发工作流中进行正确的 lockfile 管理: **将所有 lockfile 提交到版本控制中:** - `package-lock.json` (npm) - `pnpm-lock.yaml` (pnpm) - `yarn.lock` (yarn) - `bun.lock` (Bun) - `deno.lock` (Deno) ## 6. 避免盲目升级 npm 包 一些开发者在持续集成过程或本地开发实践中,会将所有依赖自动升级到最新版本,目的是确保向前兼容性或保持“最前沿”。盲目的依赖升级可能会从被盗用的账户中拉取恶意包、引入功能性错误,或使应用程序面临供应链攻击,例如 colors[^4] 和 node-ipc[^5] 安全事件。 ## 7. .env 文件中不应存放明文密钥 环境变量和 `.env` 文件通常用于存储配置和敏感数据,如 API 密钥、数据库密码和 token。然而,这些密钥是以明文形式存储的,很容易被恶意的 npm 包、被盗用的依赖或获得了你开发环境访问权限的攻击者所窃取。 即使 `.env` 文件没有被提交到版本控制系统中,它们在供应链攻击中仍然是脆弱的目标,因为恶意代码可以读取进程环境变量或扫描文件系统以定位包含密钥的已知配置文件。 ### 后续安全的密钥管理资源 - Liran Tal 的 [不要在环境变量中使用密钥](https://www.nodejs-security.com/blog/do-not-use-secrets-in-environment-variables-and-here-is-how-to-do-it-better) - 1Password 的 [使用 1Password CLI 自动化密钥管理](https://developer.1password.com/docs/cli/get-started/) - Infisical 的 [开始使用 Infisical CLI](https://infisical.com/blog/stop-using-env-files) ## 8. 在 Dev Containers 中工作 [开发容器](https://code.visualstudio.com/docs/devcontainers/containers)(dev containers)提供了一个隔离的沙盒环境,可限制供应链攻击的爆炸半径。当恶意的 npm 包在安装或运行时执行时,它们会被限制在容器环境中,而无法访问你的整个宿主系统,因为你的宿主系统中可能运行着其他项目、敏感文件或个人数据。 ### 后续资源 - 关于[为 Node.js 本地开发设置 Dev Containers 和 1Password Secrets](https://www.nodejs-security.com/blog/mitigate-supply-chain-security-with-devcontainers-and-1password-for-nodejs-local-development) 的分步指南 - 考虑对 Dev Container 进行进一步的加固: ``` "runArgs": [ "--security-opt=no-new-privileges:true", "--cap-drop=ALL", "--cap-add=CHOWN", "--cap-add=SETUID", "--cap-add=SETGID" ], "containerEnv": { "NODE_OPTIONS": "--disable-proto=delete" }, ``` - 考虑使用自定义 Dockerfile 以增强安全性 ## 9. 为 npm 账号启用双重身份验证 (2FA) 2018 年的 eslint-scope[^6] 事件证明了被盗用的 npm 账户存在的风险,当时攻击者在窃取了开发人员的凭据后发布了恶意代码。双重身份验证通过要求在用户名和密码之外提供额外的验证,为防范此类攻击提供了必要的保护。 ## 10. 发布时附带来源证明 来源声明提供了关于你的包是在哪里以及如何构建的加密证明,在源代码和发布的包之间建立了可验证的联系。这种透明度有助于用户验证包的真实性并检测篡改。 ## 11. 使用 OIDC 发布 受信发布方式通过使用来自 CI/CD 环境的 OpenID Connect (OIDC) 身份验证,消除了对长期有效的 npm token 的需求。这种方法使用短暂的、经过加密签名的 token,这些 token 专属于你的工作流,不能被提取或重用。这种 npm 包发布方法被严格限制为只允许从你受信的 CI 环境(GitHub Actions 或 GitLab)和你特别授权的工作流文件中进行发布。 受信发布支持 GitHub Actions 和 GitLab CI/CD,并会自动生成符合 OpenSSF 标准的来源证明。 ## 12. 精简包依赖树 最小化依赖可以降低安全风险、提高性能,并减少遭遇供应链攻击的可能性。更少的依赖意味着更少的潜在故障点,并降低了依赖树中包含恶意包的风险。 现代 JavaScript 提供了许多以前需要外部库才能实现的内置功能。在添加任何依赖之前,请考虑维护负担、安全影响以及包体积影响。 ## 13. 查阅 Snyk 安全数据库了解包健康状况 包的健康不仅仅包含已知的漏洞——它还包括维护活动、社区采用率、流行度趋势和安全态势。一个很少维护或社区正在萎缩的包,未来被攻破或被废弃的风险可能更大。 ## 14. 不要盲目信任官方 npmjs.org registry 官方 [npmjs.org](https://www.npmjs.com) 注册中心网站并未完整展示 npm 包的所有信息。例如,即使包的 `package.json` 文件中声明了基于 Git 和 HTTPS 的依赖,npmjs.org 网站也会将它们省略。这意味着一个包可能包含注册中心网站上的用户无法看到的非注册中心依赖。 此外,已经有证据表明,npmjs.org 网站上显示的源代码,可能会与运行 `npm install` 时实际安装的 tarball 内容产生偏差。这意味着你在网站上审查的代码可能并不是最终下载到你机器上的代码。 ## 作者 **npm 安全最佳实践** © [Liran Tal](https://github.com/lirantal),在 [Apache 2.0](./LICENSE) 许可证下发布。 [^1]: [Shai-Hulud:npm 生态系统中的大规模后门](https://snyk.io/blog/embedded-malicious-code-in-tinycolor-and-ngx-bootstrap-releases-on-npm/) [^2]: [在流行的 Nx 开发工具中发现恶意代码](https://snyk.io/blog/weaponizing-ai-coding-agents-for-malware-in-the-nx-malicious-package/) [^3]: [Event-Stream 事件事后分析](https://snyk.io/blog/a-post-mortem-of-the-malicious-event-stream-backdoor/) [^4]: [Colors 包事件](https://snyk.io/blog/open-source-npm-packages-colors-faker/) [^5]: [Node-ipc 事件](https://snyk.io/blog/peacenotwar-malicious-npm-node-ipc-package-vulnerability/) [^6]: [Eslint-scope 事件](https://eslint.org/blog/2018/07/postmortem-for-malicious-package-publishes/)
标签:Bun, CLI, GitHub Advanced Security, GNU通用公共许可证, MITM代理, Node.js, npm, pnpm, WiFi技术, Yarn, 依赖安全, 前端安全, 包管理器, 安全加固, 安全最佳实践, 开发安全, 数据集, 暗色界面, 统一API, 网络安全, 隐私保护