strix 渗透测试技能实战:npx Confusion(包运行器身份混淆)的检测、验证与上报
【免费下载链接】strixOpen-source AI penetration testing tool to find and fix your app’s vulnerabilities.项目地址: https://gitcode.com/GitHub_Trending/strix/strix
本文系统讲解 strix 开源项目中的custom/npx_confusion安全测试技能(对应文件 strix/skills/custom/npx_confusion.md),它用于发现"包运行器(package runner)实际执行了与工作流预期来源不一致的包"这一供应链身份混淆问题。读完本文,你将掌握在npx/npm exec/bunx/yarn dlx/pnpm dlx/deno run npm:等场景下判定核心条件、复现真实解析链、核对 registry 与所有权状态、按 strix 上报门禁产出漏洞报告并规避误报的完整方法。
技能定位:它解决什么问题、何时启用
在 strix 中,skills 是注入给 Agent 的知识包:每次创建 Agent 最多加载 5 个与子任务相关的技能(由 strix/skills/init.py 的validate_requested_skills校验),技能 Markdown 经 YAML frontmatter 解析后动态注入 Agent 的系统提示。本技能属于/custom(社区贡献 / 行业特定场景)分类,其 frontmatter 给出了精确的启用条件:
"Test package and executable identity confusion in npx, npm exec, and bunx fallback, plus explicit auto-fetch runners such as pnpm/yarn dlx and deno run npm:, with runner-specific resolution analysis, registry-state controls, reporting gates, and false-positive elimination"
使用时机:当某个包运行器可能执行了工作流真正想用的发布者/包之外的代码时启用。核心场景分为两类:
- 本地优先回退型(
npx、npm exec、bunx):最常见的形态是"本地缺少某个可执行文件"被重新解释为"远程拉取的包规范(package spec)"。 - 显式自动拉取型(
pnpm dlx、yarn dlx、deno run npm:):语义本身不同,应把它们当作"相邻的包身份问题"来分析,而不是假装它们共享 npm 的回退顺序。
联动技能:文档要求同时考虑四个相邻技能以完成闭环分析——dependency_cve_scanning(已知漏洞版本)、infrastructure_lifecycle(废弃域名或 registry 资源)、agentic_system_security(MCP/Agent 进程的权威与继承权限)、semantic_confusion(通用的查找顺序模型)。
核心条件:先判断命中哪条 Runner 分支
分析第一步是按 runner 选择分支,而不是把各工具的解析语义混为一谈。
本地优先回退型(npx / npm exec / bunx)
必须同时满足全部 5 项才算命中:
- 目标控制的某个工作流调用了裸可执行名或歧义包 token;
- 预期包的名称与其暴露的可执行名不一致,或存在其他证据能确定预期的发布者/包;
- 该可执行文件在该工作流真实的本地、工作区、全局或缓存上下文中(按具体 runner 判定)无法被解析;
- runner 因此从其配置的 registry 中选择了一个非预期的远端包规范;
- 受影响工作流以具备安全相关权限的身份执行到了该包的二进制。
显式自动拉取型(pnpm dlx/pnx/pnpx、yarn dlx、deno run npm:)
不要要求或声称存在"本地缺二进制后回退"这一条件。需要的是证明命令所命名或推断出的包不同于工作流真正想要的包,典型诱因包括:
- scope 包与其 bin 名称不匹配(scoped-package/bin mismatch);
- 拼写错误(typo);
- 生成的配置错误;
- 错误的发布者。
之后必须证明:实际拉取的确切包、选中的二进制/模块、执行路径、以及继承到的权限。
⚠️ 边界声明:一个公开包只是"不在目标的所有权范围内"不构成漏洞。第三方包是正常现象——漏洞点永远是"预期可执行文件的来源(provenance)与实际 registry 解析结果之间的不匹配"。
解析模型(Resolution Model)
由于 npm 7 起npx已改用npm exec的实现,且解析器行为会随版本变化,必须记录 npm 版本。对 npm 需建模以下决策链:
bare command -> executable in ancestor node_modules/.bin? -> executable in global bin? -> matching local/global package and usable bin? -> matching environment in the npx cache? -> treat the command token as a package spec -> fetch its manifest from the configured registry -> infer one executable from package.json#bin -> install into the npx cache and execute此外还要记录上下文状态,因为它们都直接影响结果:
- 工作目录与 workspace 根;
- 本地依赖树与生成的
node_modules/.bin链接; - 全局 prefix/bin 目录与 npx 缓存;
registry、scope 级 registry 规则、代理与鉴权配置;- 命令形式、flags、包规范/版本、TTY/CI 状态与
yes策略; - 当包暴露 0 个、1 个或多个
bin条目时 npm 的可执行文件推断结果。
不要把"包名查找"和"bin 选择"折叠成一步——npm 可能拉到了 manifest,却因无法唯一推断一个可执行文件而失败,这两步是独立的失败点。
Runner 差异对照
必须记录确切的 runner 与其版本,不得把 npm 的本地/全局/缓存顺序套用到其他实现上:
| Runner | 需要建模的解析行为 | 包绑定 / 拉取控制 |
|---|---|---|
npx/npm exec | 本地/工作区/全局/缓存解析,随后包规范回退;可执行文件推断依赖package.json#bin | --package <pkg>绑定提供者;--no拒绝安装提示 |
bunx | 先查本地已装包,随后可从 npm 安装到 Bun 缓存 | --package <pkg>绑定提供者;--no-install禁止安装 |
yarn dlx | 默认将命令名包下载到临时环境;这不是本地 bin 回退 | --package <pkg>选择不同的提供者包 |
pnpm dlx/pnx/pnpx | 拉取并热加载 registry 包后运行其默认二进制;项目信任策略随版本而异 | --package=<pkg>选择提供者;当远端拉取非本意时,优先声明依赖 +pnpm exec |
deno run npm:<pkg> | 使用显式 npm 包规范与缓存;子路径可选择二进制 | 固定包/子路径,并建模 lock、cache、生命周期脚本与 Deno 权限设置 |
另外,latest、next、@2、caret(^)、tilde(~)这类可变 tag 与范围是选择器(selector),不是锁定(pin)。需要重复执行的高权限工作流,在 runner 支持的前提下,必须使用经过审查的精确版本外加 lockfile/完整性校验。
高信号模式(High-Signal Patterns)
裸可执行文件回退
npx internal-tool npx -y internal-tool npm exec -- internal-tool该信号在CI、发布脚本、引导命令、开发者环境搭建、工具/Agent 配置中最强——尤其是同一命令会被反复执行的场景。
Scoped 包与无 scope 的 bin
scope 包可以暴露无 scope 的可执行名。例如目标工作区正确安装了下面的包:
{ "name": "@org/tooling", "bin": { "org-tool": "./bin/run.js" } }在正确安装的 workspace 内部,npx org-tool可能解析到node_modules/.bin/org-tool;但在该目录树之外,同一命令可能回退到名为org-tool的公开包。必须把文档、MCP 配置、引导脚本视为彼此独立的执行上下文,不要假设仓库内的解析结果在所有地方都成立。
Agent 与 MCP 启动器
检查.mcp.json、编辑器/桌面 Agent 配置、devcontainer、以及生成的工具启动器中是否存在command: npx+-y+ 裸包名/二进制名的组合。此类分析要与 agentic_system_security 结合,才能判断该进程继承的凭据、工具、文件与网络访问权。
候选收集(Candidate Collection)
对可执行面进行全文搜索,并保留文件、行号、命令、执行上下文四元信息:
rg -n --no-heading -g '!node_modules' -g '!**/dist/**' \ -e '\b(npx|npm\s+exec|bunx|pnx|pnpx|pnpm\s+dlx|yarn\s+dlx)\s+[^[:space:]]+' \ -e '\bdeno\s+run\b[^\n]*\bnpm:' \ -e '"command"\s*:\s*"(npx|bunx|pnx|pnpx|pnpm|yarn|deno)"' \ -e '"args"\s*:\s*\[[^]]*"(dlx|npm:[^"]+|-y)"' \ .搜索源码/配置文件树而非固定文件清单——这些命令同样可能出现在:
scripts/、husky/lint-staged 钩子;turbo.json/nx.json任务定义;.circleci/、composite-action 的action.yml;- devcontainer 的
postCreateCommand; - 嵌套 workspace 的各
package.json; - 编辑器/Agent 配置:
.cursor/、.vscode/、.mcp.json。
若生成的输出本身会被发布或执行,请单独搜索其所在目录,而不是全局包含所有dist/产物。还需检查:package scripts 与生命周期钩子、workspace 包的name/bin映射、README 与生成的搭建说明、CI composite actions 与可复用 workflows、以及可能暴露内部命令的 source maps 或打包元数据。
最后剔除不会被执行、也不以可执行命令形式呈现的内容:路径、shell 变量、flags、Node 内置模块名、普通文本。
确认真实解析结果(Establish the Actual Resolution)
优先检查已存在的依赖树、lockfile、workspace 包与.bin链接。不要为了判断某命令是否本地可用就跑npm ci——它会改变依赖树,还可能执行生命周期脚本。
若需要可版本控制的复现环境,用以下命令记录 npm 的 registry 查询(同时禁止安装缺失包):
npx --no --loglevel=http <candidate>解读结果时必须谨慎,不能过度推论:
- 本地可执行文件可能立即运行;
--no只拒绝"缺失包安装"; - 出现 HTTP registry 请求只说明发生了回退,既不代表所有权,也不代表成功执行;
- 取消安装时提示出的缺失包名,恰恰揭示了 npm选中的包规范;
- 缓存、全局安装、父目录、workspaces 与 registry 配置都可能改变结果。
在每个关键上下文里都重复做解析分析:仓库根目录、文档记载的启动目录、CI checkout、生成的 Agent 配置、以及"装依赖之前的引导流程"。不要用一个干净的空目录替代目标上下文——除非你只是想理解 npm 的通用名称映射。
⚠️ 不要把
npx --no当成通用 dry-run 参数。Bun 的本地解析问题只能用bunx --no-install。dlx与deno run npm:本来就显式命名了一个可远端解析的包,因此要按其自身行为校验包规范、registry、缓存/lock、选中的二进制或子路径与权限。
所有权与 Registry 状态核对
查询目标配置实际选中的 registry,然后区分以下状态:
- 预期发布者拥有的预期包;
- 同名但无关的公开包;
- 未注册名(正常 registry 返回
404); - 私有或访问受限名(
401/403); - 瞬时/限速/被拦截的查询(
429、5xx、超时); - 占位、被保留、有争议或曾被取消发布的名称。
在信任上述任何状态前,先确认目标的查询路径能否区分"已知存在的包"与"新建的阴性对照(negative control)"。解析 registry 时必须使用与目标相同的工作目录与配置:
# Public npm example; use a known package from the actual registry when different. task_registry="$(npm config get registry)" npm view --registry="$task_registry" lodash name --json npm view --registry="$task_registry" "$(openssl rand -hex 12)" name --json让这一对照组合穿过与候选目标相同的.npmrc、scope 路由、鉴权、代理与出网路径。直接对公开 registry 发curl请求是另一组独立观测,除非目标 runner 恰好就走那条路由。成功的一对结果只建立粗粒度的正/负区分能力,并不代表每个候选响应都真实可信——还需验证返回的文档确实命名了所请求的包,并含有可信的 registry 元数据。
如果对照组合失败或返回无法区分的响应,就把目标路径的 registry 状态标记为UNKNOWN。独立验证过的公开 registry 响应可以描述公开状态,但不能证明目标 runner 解析到的内容。在依赖"候选不存在"这一结论前,必须重新确认。
404只能证明该包在当时不存在于该 registry,并不能单独证明注册会被受理。registry 相似度、商标、保留、安全冻结、取消发布规则都是独立事实。文档给出两个必须实测、不可臆断的具体案例:
- registry 自带的安全占位包会占据名称,即便其唯一版本是
0.0.1-security。不要只凭版本号识别占位包——要检查 packument、description、dist-tags、顶层与版本级 maintainers、以及_npmUser等版本发布者信息。 - npm 会拒绝与现有包在去掉
.、-、_之后发生碰撞的新无 scope 名称。因此要双向归一化候选名与既有名:只查候选名的剥离形式能抓到some-tool对sometool,但会漏掉反向情况(既有包名含标点)。这属于 registry 策略的资格证据,而非注册必然成功的保证。
当候选名已被注册时,先区分是目标自己的组织还是无关第三方,再谈冲突:交叉比对npm owner ls <name>、版本级发布者元数据、已知的目标控制 npm 组织、以及独立验证过的仓库出处。仓库/homepage 字段只是自述式佐证,不能单独定所有权。若发布者身份仍模糊,标记为UNKNOWN。
验证与影响(Validation and Impact)
必须演示一条完整的解析器陈述链:
target-controlled invocation and context -> intended executable absent -> exact public package spec selected -> package ownership/availability state -> execution trigger and inherited authority两条红线:没有执行路径就不要上报未注册名称;在任何相关上下文里命令都可由本地满足时,也不要上报执行路径。影响等级要从实际执行该包的环境推导:开发者工作站、CI 任务、发布流水线、Agent 运行时、容器构建、或仅文档类工作流——执行环境不同,影响模型完全不同。
上报门禁(Reporting)
这里的发现没有 CVE、也没有"已安装的漏洞版本",所以不走create_dependency_report——该工具要求 advisory 匹配的 CVE(详见 dependency_cve_scanning 技能)。只有在对应核心条件被完整验证后,才使用create_vulnerability_report。
证据的最低标准
一次 registry 查询或一个404只是候选证据,不是可工作的 PoC。报告必须:
- 保留目标调用方式与执行上下文;
- 展示被选中的确切包与二进制/模块;
- 在可代表原环境的受控复现设置中演示 runner 的执行转移,同时不得发布有争议的名称;
- 建立该进程继承到的权限;
- 有源码时,在
code_locations中给出负责的调用/配置与具体修复。
结合 strix/tools/reporting/tool.py 的实现看,create_vulnerability_report对code_locations的校验恰好呼应了上述要求:每个条目需要相对路径file与正整数start_line(_validate_file_path会拒绝绝对路径与含..的路径),且当条目携带可应用的fix_after时还强制要求fix_verification陈述。因此**"保留调用与上下文 + 给出一致可定位的修复位置"不是可选建议,而是工具层面的硬约束**。
不要上报的类型
以下内容不要上报,仅在有用时留作调查笔记:
- 仅文档/注释中的引用;
- 本地已满足的命令;
- 无法注册的名称;
- 所有权模糊;
- 在包执行前就断掉的链条。
严重度推导
CVSS 必须从已证实的路径推导,而非套用固定严重度标签。需要考虑:所需的开发者/用户操作、registry 与配置前提、runner 权限、凭据可得性,以及实际暴露的机密性/完整性/可用性。CI、release、容器构建或 Agent 上下文可能很严重,但"上下文名称"本身不足以定 High/Critical——这一立场与 reporting 工具 的校验逻辑一致:cvss_breakdown的 8 个指标(attack_vector/attack_complexity/privileges_required/user_interaction/scope/confidentiality/integrity/availability)各有合法取值集合,分数由向量计算得出,无法凭空填一个分数。
去重
按根因、受影响资产/工作流、修复方式去重:
- 相同的配置错误与修复方案作用于多处调用点时,合并为一个发现;
- 同一候选名影响不同产品、租户、runner 语义、权限或修复时,分别保留。
误报清单(False Positives)
以下情形不应判定为漏洞,逐条核对后再上报:
- 每个真实执行上下文中,可执行文件均由已声明依赖提供;
npx --package @scope/pkg <bin>显式把二进制绑定到预期包;- 版本化包规范或 scope 级 registry 指向预期发布者;
- 该公开包就是刻意选中的第三方工具;
- npm 拉到了 manifest 但无法推断或执行 bin;
- 引用只出现在生成的/压缩文本中,没有可执行调用点;
- registry/代理错误被误读为未注册名,或目标路径的对照组合不具决定性;
- 包虽不存在,但 registry 策略阻止了该争议名称的注册;
- 命令解析到的是刻意选中的生态工具与预期发布者;
- 已注册名称属于目标自己的组织;
- 显式
dlx或npm:包规范被当作"本地缺失回退",却没有包/发布者不匹配的证据。
修复方案(Remediation)
| 场景 | 修复做法 |
|---|---|
| 通用 | 安装预期包,并通过 npm script 调用其本地可执行文件 |
| npm | 绑定并固定提供者:npx --package @org/tool@<version> org-tool;当缺失依赖必须失败时使用--no |
| Bun | 用bunx --package @org/tool@<version> org-tool;不打算远端安装时加--no-install |
| yarn / pnpm dlx | 在可重复或高权限工作流中,用已声明并锁定的依赖+ 各 runner 的本地exec命令替代;确需临时执行时,显式绑定并固定提供者包 |
| Deno | 固定npm:包与二进制子路径,保留经审查的 lockfile,适当场景走纯缓存运行,并只授予命令所需的最小权限 |
| 私有 scope | 将私有 scope 路由到预期 registry,阻止公开回退 |
| 通用(版本) | 在高权限工作流中固定包版本与 lockfile |
| Agent 启动器 | 将裸npx -y <name>启动器替换为经审查、限定发布者、锁定版本的包规范 |
小结
把包运行器混淆当作一个身份与执行上下文缺陷来处理,而不是一个"装了个坏包"的问题:
- 证明 runner 特定的转移——按各自解析顺序建模,禁止跨 runner 套用 npm 的回退顺序;
- 区分二进制名与包名——scope 包可暴露无 scope 的 bin,两个名字是两回事;
- 核实 registry 与发布者状态,但不把"不存在"等同于"可注册"——用正/负对照、双向名称归一化、占位包特征与 owner 取证得出
UNKNOWN之外的结论; - 只上报受影响工作流真实权限下的完整执行路径——Registry 查询与 404 只是候选证据。
在整个分析流程中,本技能与 semantic_confusion 的"命名空间与解析回退"模型、agentic_system_security 的进程权威分析、以及 dependency_cve_scanning 的已知 CVE 通道互为补充:前者关注"一次查找跨多作用域后的来源漂移",后者才负责"已发布 advisory 命中的已知漏洞版本",两者不要混淆上报通道。
【免费下载链接】strixOpen-source AI penetration testing tool to find and fix your app’s vulnerabilities.项目地址: https://gitcode.com/GitHub_Trending/strix/strix
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考