拓冰建站拓冰建站
首页 / 资讯中心 / 正文

pnpm POSIX bin shim 安全加固:深入解析 `command -p` 与依赖 bin 抢占问题(14837)的修复

pnpm POSIX bin shim 安全加固深入解析command -p与依赖 bin 抢占问题#14837的修复【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm本篇技术指南围绕 pnpm 仓库中 .changeset/shim-helpers-bypass-caller-path.md 所记录的变更展开pnpm 为 Linux/macOS 等 POSIX 环境生成的.bin命令 shim 曾通过PATH环境变量查找其运行所需的 shell 辅助工具readlink、sed、printf、uname而依赖包自身的 bin 恰好位于PATH最前端从而可以伪装成这些辅助工具并劫持其他包的命令 shim。阅读本文后你将理解 pnpm shim 的内部结构与执行流程、该安全缺陷的完整攻击面、command -p修复方案的底层原理以及在 Cygwin、MSYS2、WSL 等平台上的残余边界与热更新替换机制。一、背景pnpm 为什么需要 bin shim在 npm 生态中一个包可以通过package.json的bin字段声明命令行工具pnpm 安装时会在node_modules/.bin目录下为这些命令创建可执行入口。由于安装的目标文件例如 TypeScript 的typescript/bin/tsc通常只是一个带 shebang 的脚本且其真实路径深藏在虚拟存储目录virtual store中pnpm 需要生成一段包装脚本——即 shim——来解析真实目标并正确启动它。从仓库源码结构看shim 的生成与链接逻辑集中分布在两套实现中Rust 实现新架构pnpm/crates/cmd-shim/src/shim/sh.rs 负责生成 POSIX/bin/shshim配套的还有 powershell.rs 生成 PowerShell shim整体编排见 pnpm/crates/cmd-shim/src/lib.rs。TypeScript 实现pnpm 11 及历史版本pnpm11/bins/cmd-shim/src/index.ts 中同时维护着三套 shim 模板POSIX sh、Windows.cmd、PowerShell.ps1供 pnpm11/bins/linker/src/index.ts 链接器在安装时写入磁盘。一个典型的 POSIX shim 大体长这样以tsc为例目标为node_modules/typescript/bin/tsc#!/bin/sh # 解析 $0 的符号链接链得到 shim 真实所在目录作为 basedir link$0 hops0 while [ -L $link ] [ $hops -lt 40 ]; do hops$((hops1)) target$(command -p readlink $link) ... done basedir$(command -p printf %s\n $link | command -p sed -e s,\\,/,g) basedir${basedir%/*} ... if [ -x $basedir/node ]; then exec $basedir/node $basedir/../typescript/bin/tsc $ elif command -v node /dev/null 21; then exec node $basedir/../typescript/bin/tsc $ ... fi # cmd-shim-target/proj/node_modules/typescript/bin/tsc它的工作分为三段解析$0定位真实目录basedir、在 MSYS/WSL 分支下做 Windows 路径换算basedir_win、通过exec启动目标脚本。二、缺陷分析依赖 bin 如何抢占其他包的 shim2.1 问题根因shim 运行时的PATH环境pnpm 在安装时会把node_modules/.bin置于进程的PATH最前端这正是用户能直接敲tsc、eslint等命令的原因。但这也意味着shim 脚本一旦通过PATH去查找任何可执行工具都会先命中node_modules/.bin里的东西——而那里恰好装着所有依赖包声明的 bin。旧版 shim 的头部正是这样实现的readlink、sed、uname、printf等辅助工具都是通过裸名称在PATH中解析的。于是攻击面成立某个依赖包甚至是恶意包只要在自己的bin字段里声明一个名为readlink或sed的可执行文件pnpm 就会把它链接进node_modules/.bin当其他包如 typescript的 shim 运行时shell 会在PATH中先找到这个伪造的readlink/sed从而伪造readlink的输出把$0指向攻击者控制的目录伪造sed的输出破坏basedir的路径换算伪造uname的输出诱导 shim 进入 MSYS/WSL 分支篡改 Windows 路径转换结果。最终 shim 会exec一个攻击者指定目录下的脚本——也就是本变更记录所指的A dependencys own bins can no longer take over another packages bin shim依赖自身的 bin 不能再抢占另一个包的 bin shim。该问题对应 upstream issue #14837。2.2 攻击的具体路径basedir计算链在 pnpm/crates/cmd-shim/src/shim/sh.rs 的SH_SHIM_HEADER第 136-187 行中可以看到旧链路的关键依赖target$(readlink $link) # 解析符号链接目标 basedir$(printf %s\n $link | sed -e s,\\,/,g) # 反斜杠转正斜杠 case uname -a in ... esac # 检测 Cygwin/MSYS/WSL上述三个辅助工具若从PATH解析均可被node_modules/.bin中的同名 bin 顶替。而原本用于取目录的dirname命令同样是高危点——这正是 tests.rs 中generate_sh_shim_matches_pnpm_typical_case测试断言!body.contains(dirname)header 不得再 fork dirname 进程的原因。三、修复方案command -p强制系统默认路径3.1 核心改动本变更的核心修复是shim 头部的所有 shell 辅助工具统一改用command -p前缀调用。POSIX 规范中command -p的含义是使用系统的默认PATH值而非当前环境变量PATH来查找命令从而绕开用户或攻击者注入的目录。修复后的 header 关键行Rust 版见 pnpm/crates/cmd-shim/src/shim/sh.rs 第 140-161 行TypeScript 版见 pnpm11/bins/cmd-shim/src/index.ts 第 386-418 行两套实现保持字节级一致# A shim runs with node_modules/.bin at the front of PATH, so readlink, sed, # uname, and printf go through command -p, which searches the system default # path instead. A dependencys bin cannot stand in for one of them and take over # the shim before it reaches its target. Directories come from ${link%/*}, # which needs no helper at all. link$0 case $link in */*|*\\) ;; *) link./$link ;; esac hops0 while [ -L $link ] [ $hops -lt 40 ]; do hops$((hops1)) target$(command -p readlink $link) case $target in /*) link$target ;; *) link${link%/*}/$target ;; esac done basedir$(command -p printf %s\n $link | command -p sed -e s,\\,/,g) basedir${basedir%/*}对照旧的实现修复点可以归纳为辅助工具旧用法新用法作用readlinkreadlink $linkcommand -p readlink $link解析$0符号链接链最多 40 跳对应内核 ELOOP 上限printfprintf %s\n $linkcommand -p printf %s\n $link以不吞反斜杠的方式输出$link避免echo吃掉 Windows 路径中的\sedsed -e s,\\,/,gcommand -p sed -e s,\\,/,g将basedir中的反斜杠转换为正斜杠unameuname -acommand -p uname -a检测 Cygwin / MINGW / MSYS / WSL2决定 Windows 路径换算分支此外还有一个静默的替换原来用dirname取目录的逻辑被替换为纯 shell 参数展开${link%/*}Directories come from${link%/*}, which needs no helper at all——字符串截取不产生任何子进程自然无从劫持。3.2 为什么不用环境变量白名单command -p在大多数 Unix shelldash、bash、sh中都是 POSIX 规定的内建机制其查找路径是编译期固化在 shell 里的系统默认值不依赖进程环境。相比在 shim 里硬编码绝对路径如/usr/bin/readlinkcommand -p既保持了对不同发行版路径差异的兼容性又彻底摆脱了PATH污染——这是该修复选型的关键。3.3printf替换echo的连带加固顺带一提header 同时把echo $link换成了command -p printf %s\n $link。原因在 tests.rs 的posix_shim_header_normalizes_windows_backslash_paths_without_echo_escapes测试中有明确说明POSIXecho会把\n、\t解释成换行和制表符当$0是C:\node_modules\.bin\tsc这类 Windows 形式路径时反斜杠会在sed转换前就被破坏。这与本次辅助工具来自 PATH的修复同属 shim 健壮性加固的一部分。四、修复的边界Cygwin、MSYS2、WSL 上的残余风险本变更记录特别指出一处有意保留的行为在 Cygwin、MSYS2、WSL 上shim 的 Windows 路径转换工具——cygpathCygwin/MSYS2 分支与wslpathWSL2 分支——仍然从PATH中查找因此在这些环境中依赖包仍然可以伪造cygpath/wslpath来重定向 shim 的 Windows 路径换算。对应代码在 pnpm/crates/cmd-shim/src/shim/sh.rs 第 167-185 行case command -p uname -a in *CYGWIN*|*MINGW*|*MSYS*) if command -v cygpath /dev/null 21; then basedir_wincygpath -w $basedir fi exe.exe msystrue ;; *WSL2*) if command -v wslpath /dev/null 21; then basedir_win$(wslpath -w $basedir 2 /dev/null) if [ $? -ne 0 ] || [ -z $basedir_win ]; then basedir_win$basedir else exe.exe fi fi ;; esac可以看到cygpath与wslpath的查找仍使用裸command -v即服从当前PATH。理解这一边界需要结合修复目的readlink/sed/printf/uname是 shim 达成正确启动目标的必经之路被劫持会直接改变最终exec的可执行文件而cygpath/wslpath仅在跨平台路径转换场景生效属于相对次要的窗口。从源码结构看这是修复方在彻底加固与保持跨平台兼容之间作出的明确取舍而非疏漏——测试 tests.rs 中generate_sh_shim_uses_windows_target_only_for_exe_branches等用例专门钉住了 Windows 形式路径只在.exe分支使用的行为。五、热更新重新安装时如何替换存量 shim变更记录中提到Reinstalling replaces the shims already in yournode_modules重新安装会替换 node_modules 中已存在的 shim。实现层面pnpm 通过两条机制判断一个存量 shim 是否陈旧5.1 目标标记# cmd-shim-targetrel生成的 shim 末尾带一行尾部标记见 pnpm/crates/cmd-shim/src/shim/sh.rs 第 231-236 行的shim_target_marker与第 242-244 行的is_shim_pointing_at# cmd-shim-target/proj/node_modules/typescript/bin/tscis_shim_pointing_at通过比对这行标记判断存量 shim 是否仍然指向同一目标从而在暖安装warm reinstall时短路重写。5.2 加固判定is_sh_shim_hardened但目标没变不代表 header 是安全的——旧 header 同样指向正确目标。因此 pnpm/crates/cmd-shim/src/shim/sh.rs 第 257-273 行提供了is_sh_shim_hardened逐行检查 shim 是否包含钉死的两行内容——target$(command -p readlink $link)SH_SHIM_HARDENED_HELPER_LINE与basedir$(command -p printf ...)SH_SHIM_PATH_PRINTF_LINE。只要缺其中任何一行下次安装就会重写该 shim前者缺失意味着辅助工具仍可能从PATH被劫持后者缺失意味着 Windows 形式的$0仍可能被echo吞掉反斜杠。配套测试 tests.rs 第 69-90 行的generate_sh_shim_header_carries_the_hardened_helper_line同时做了双向钉扎新生成的 shim 必须被判定为 hardened把command -p readlink改回裸readlink、或把printf管道换回echo后则必须判定为未加固。六、测试验证从单元测试到端到端劫持演练本次修复的验证不仅停留在静态断言pnpm/crates/cmd-shim/src/shim/tests.rs 中还有一个完整的 Unix 端到端测试shim_execution_ignores_helpers_from_the_callers_path第 712-719 行真实地模拟了劫持场景plant_shimmed_tool第 737-756 行在临时目录搭出node_modules/.bin/tscshim 与真实目标typescript/bin/tsc.js并在.bin中埋入一个名为node.exe的伪装 bin注释明确说明A dependency can declare a bin namednode.exeplant_hijack_tree_and_decoys第 778-798 行再构造一个劫持树在PATH最前端放置伪造的readlink、sed、printf、dirname、uname脚本其中uname直接输出MINGW64_NT-10.0以诱导 MSYS 分支其余伪造工具会把$0指向劫持者控制的tsc.jsassert_shim_reaches_its_target第 761-772 行把伪装目录置于PATH最前运行 shim断言其 stdout 必须是真实目标的tsc-output——若 shim 曾从PATH取用任何一个辅助工具输出就会被替换为hijacked测试即失败。这套测试同时覆盖了shim 通过绝对路径执行与通过相对符号链接tsc-link执行两种入口从执行层面证明了修复后 shim 对调用者PATH中的伪装工具完全免疫。七、总结与升级建议本变更是一次典型的安装器安全加固根因是 shim 在运行时对PATH的隐式信任修复方式是用 POSIXcommand -p把所有必经辅助工具切换到系统默认路径查找并用纯 shell 参数展开取代dirname。它带来的直接影响包括依赖包含恶意包的 bin 无法再伪装成readlink/sed/printf/uname来劫持其他包的命令 shim存量 shim 通过is_sh_shim_hardened判定并在下次安装时被自动替换无需用户手动清理node_modules在 Cygwin/MSYS2/WSL 上cygpath/wslpath的 Windows 路径转换仍服从PATH属于该修复的已知边界。对于使用 pnpm 的开发者本修复无需任何手动操作——只需正常执行一次安装pnpm install或pnpm install --force让新的 shim 落盘即可。若你的项目运行在 CI 或 Docker 环境中并存在多租户安装不同项目共享镜像层建议关注本变更对应的发布版本号确保升级后触发一次全量重链使存量 shim 尽快获得加固。如果你想进一步研究 shim 的实现细节建议从以下仓库路径继续深入POSIX shim 生成与 header 常量见 pnpm/crates/cmd-shim/src/shim/sh.rs三平台 shim 模板的 TypeScript 实现见 pnpm11/bins/cmd-shim/src/index.ts链接器如何编排写入见 pnpm/crates/cmd-shim/src/link_bins 与 pnpm11/bins/linker/src/index.ts安全测试用例见 pnpm/crates/cmd-shim/src/shim/tests.rs 和 pnpm/crates/cmd-shim/src/link_bins/tests/security.rs。【免费下载链接】pnpmFast, disk space efficient package manager项目地址: https://gitcode.com/gh_mirrors/pn/pnpm创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门