diff --git a/strix/skills/README.md b/strix/skills/README.md index 1f9dc007..5173ab37 100644 --- a/strix/skills/README.md +++ b/strix/skills/README.md @@ -42,7 +42,7 @@ Notable source-aware skills: - `source_aware_whitebox` (coordination): white-box orchestration playbook - `source_aware_sast` (custom): semgrep/AST/secrets/supply-chain static triage workflow - `dependency_cve_scanning` (custom): trivy-based SCA workflow for reporting known dependency CVEs via `create_dependency_report` -- `npx_confusion` (custom): npx/bunx/dlx binary-name fallback into unintended public package resolution, with registry claimability controls and reporting gates +- `npx_confusion` (custom): npx/npm exec/bunx fallback and adjacent package-runner identity confusion, with runner-specific registry and reporting gates - `advisory_to_poc` (custom): advisory-to-root-cause workflow for patch diffing, public PoCs, and detector design - `appliance_firmware` (technologies): appliance artifact, runtime, and install-state analysis - `protocol_reverse_engineering` (protocols): stateful/custom protocol reconstruction and safe harnessing diff --git a/strix/skills/custom/npx_confusion.md b/strix/skills/custom/npx_confusion.md index 1ae801f4..3dd1ca32 100644 --- a/strix/skills/custom/npx_confusion.md +++ b/strix/skills/custom/npx_confusion.md @@ -1,29 +1,33 @@ --- name: npx-confusion -description: Test npx, npm exec, bunx, and dlx binary-name confusion where a missing local executable is reinterpreted as a public npm package name, including scoped-package bin mismatches, CI and agent invocations, resolution-context analysis, registry claimability verification, and false-positive elimination +description: 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 Confusion -Use this skill when a target invokes a bare command through `npx`, `npm exec`, `bunx`, or `pnpm`/`yarn dlx` and the intended executable name may differ from the package that provides it. This is narrower than classic dependency confusion: the issue is the transition from **unresolved binary name** to **remotely fetched package spec**. +Use this skill when a package runner may execute code from a package other than the publisher or package the workflow intended. For `npx`, `npm exec`, and `bunx`, the recurring case is a missing local executable being reinterpreted as a remotely fetched package spec. Explicit auto-fetch runners such as `pnpm dlx`, `yarn dlx`, and `deno run npm:` have different semantics; analyze them as an adjacent package-identity problem rather than pretending they share npm's fallback order. Load `dependency_cve_scanning` for known vulnerable versions, `infrastructure_lifecycle` for abandoned domains or registry resources, `agentic_system_security` for the authority of an MCP/agent process, and `semantic_confusion` for the general lookup-order model. ## Core Condition -Require all of the following: +Choose the branch that matches the runner. -1. A target-controlled workflow invokes `npx `, `npx -y `, or an equivalent auto-installing runner: `npm exec`, `bunx`, `pnpm dlx`, `yarn dlx`, or `deno run npm:`. Each fetches a package named after the command when it is not already available; confirm the specific runner's own resolution order rather than assuming npm's. -2. `` is not resolved as an executable in the workflow's real local/global context. -3. npm consequently interprets `` as a package spec and consults the configured registry. -4. The resolved public name is unintended, unregistered, or controlled by a party other than the intended publisher. -5. The affected workflow actually reaches the fetched package's executable. +For local-first fallback (`npx`, `npm exec`, or `bunx`), require all of the following: + +1. A target-controlled workflow invokes a bare executable or ambiguous package token. +2. The intended package and its executable name differ, or other evidence establishes the expected publisher/package. +3. The executable is not resolved in the workflow's real local, workspace, global, or cache context as applicable to that runner. +4. The runner consequently selects an unintended remote package spec from its configured registry. +5. The affected workflow reaches that package's executable with security-relevant authority. + +For explicit auto-fetch runners (`pnpm dlx`/`pnx`/`pnpx`, `yarn dlx`, or `deno run npm:`), do not require or claim a missing-local-binary fallback. Require evidence that the command names or infers a package different from the one the workflow intended, such as a scoped-package/bin mismatch, typo, generated configuration error, or wrong publisher. Then prove the exact fetched package, chosen binary/module, execution path, and inherited authority. A public package merely being outside the target's ownership is not a vulnerability. Third-party packages are normal; the mismatch between intended executable provenance and actual registry resolution is the finding. ## Resolution Model -Record the npm version because `npx` has used `npm exec` since npm 7 and resolver behavior changes between releases. For current npm, model these decisions: +Record the npm version because `npx` has used `npm exec` since npm 7 and resolver behavior changes between releases. For npm, model these decisions: ```text bare command @@ -48,6 +52,20 @@ Also record: Do not collapse package-name lookup and bin selection into one step. npm can fetch a manifest yet fail because it cannot infer exactly one executable. +### Runner distinctions + +Record the exact runner and version. Do not reuse npm's local/global/cache ordering for another implementation. + +| Runner | Resolution behavior to model | Package binding / fetch control | +|---|---|---| +| `npx` / `npm exec` | Local/workspace/global/cache resolution followed by package-spec fallback; executable inference depends on `package.json#bin` | `--package ` binds the provider; `--no` rejects an install prompt | +| `bunx` | Checks a locally installed package, then can install from npm into Bun's cache | `--package ` binds the provider; `--no-install` forbids installation | +| `yarn dlx` | Downloads the command-named package into a temporary environment by default; this is not a local-bin fallback | `--package ` selects a different provider package | +| `pnpm dlx` / `pnx` / `pnpx` | Fetches and hotloads a registry package, then runs its default binary; project trust policies are version-dependent | `--package=` selects the provider; prefer declared dependencies plus `pnpm exec` when remote fetch is unintended | +| `deno run npm:` | Uses an explicit npm package spec and cache; a subpath can select a binary | Pin the package/subpath and model lock, cache, lifecycle-script, and Deno permission settings | + +Treat mutable tags and ranges such as `latest`, `next`, `@2`, caret, and tilde ranges as selectors, not pins. A privileged repeatable workflow needs an exact reviewed version plus lockfile/integrity enforcement where the runner supports it. + ## High-Signal Patterns ### Bare executable fallback @@ -83,17 +101,18 @@ Search executable surfaces and retain file, line, command, and execution context ```bash rg -n --no-heading -g '!node_modules' -g '!**/dist/**' \ - -e '\b(npx|npm\s+exec|bunx|pnpm\s+dlx|yarn\s+dlx)\s+[^[:space:]]+' \ - -e '"command"\s*:\s*"(npx|bunx)"' \ - -e '"args"\s*:\s*\[[^]]*"-y"' \ + -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)"' \ . ``` -Search the whole tree rather than a fixed file list: these commands also live in +Search the source/configuration tree rather than a fixed file list: these commands also live in `scripts/`, husky/lint-staged hooks, `turbo.json`/`nx.json` task definitions, `.circleci/`, composite-action `action.yml`, devcontainer `postCreateCommand`, nested workspace `package.json` files, and editor/agent config under -`.cursor/`, `.vscode/`, and `.mcp.json`. +`.cursor/`, `.vscode/`, and `.mcp.json`. If generated output is itself shipped or executed, search its specific directory separately instead of globally including every `dist/` artifact. Also inspect: @@ -124,6 +143,8 @@ Interpret this carefully: Repeat the resolution analysis in every context that matters: repository root, documented launch directory, CI checkout, generated agent configuration, and bootstrap-before-install flow. Do not substitute a clean empty directory for the target context except to understand npm's generic name mapping. +Do not apply `npx --no` as a generic dry-run flag. Use `bunx --no-install` only for Bun's local-resolution question. `dlx` and `deno run npm:` already name a remotely resolvable package, so validate their package spec, registry, cache/lock, selected binary or subpath, and permissions using that runner's own behavior. + ## Ownership and Registry State Query the exact registry selected by the target configuration, then distinguish: @@ -135,32 +156,26 @@ Query the exact registry selected by the target configuration, then distinguish: - transient/rate-limited/blocked lookup (`429`, `5xx`, timeout) - placeholder, reserved, disputed, or previously unpublished name -Before trusting any of those states, prove the lookup path itself discriminates. -A sandboxed or proxied egress can fail uniformly, which turns every candidate -into a false unregistered name and a fabricated critical: +Before trusting any of those states, check whether the target's lookup path can distinguish a known existing package from a newly generated negative control. Resolve the registry from the same working directory and configuration used by the target: ```bash -# Positive and negative controls against the same registry, same session -curl -so /dev/null -w '%{http_code}\n' https://registry.npmjs.org/lodash # expect 200 -curl -so /dev/null -w '%{http_code}\n' https://registry.npmjs.org/$(openssl rand -hex 12) # expect 404 +# 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" "strix-control-$(openssl rand -hex 12)" name --json ``` -If the control pair does not return `200` and `404`, the egress is filtered, -mirrored, or intercepted; report nothing from ownership state until it does. -Re-confirm each `404` at least once more before relying on it. +Run the pair through the same `.npmrc`, scope routing, authentication, proxy, and egress path as the candidate. Direct `curl` requests to the public registry are a separate observation unless the target runner uses that exact route. A successful pair establishes coarse positive/negative discrimination, not authenticity of every candidate response; verify that returned documents name the requested package and contain plausible registry metadata. + +If the pair fails or returns indistinguishable responses, mark the target-path registry state `UNKNOWN`. An independently verified public-registry response may characterize public state, but it does not prove what the target runner resolves. Re-confirm candidate absence before relying on it. A `404` proves absence from that registry at that time; it does not by itself prove that registration would be accepted. Registry similarity, trademark, reservation, security-hold, and unpublish rules remain separate facts. Two concrete cases to check rather than infer: -- npm returns `200` for registry-owned security placeholders. Read `maintainers` - and the published versions: a sole `0.0.1-security` version owned by npm means - the name is taken and not claimable by anyone, including an attacker. -- npm rejects new names that collide with an existing package once punctuation - (`.`, `-`, `_`) is stripped, so a `404` name such as `some-tool` can be - unregisterable when `sometool` exists. Check the stripped form too. +- A registry-owned security placeholder occupies the name even when its only version is `0.0.1-security`. Do not identify one from the version alone: inspect the packument, description, dist-tags, top-level and version-level maintainers, and version publisher such as `_npmUser`. +- npm rejects new unscoped names that collide with an existing package after `.`, `-`, and `_` are removed. Normalize both the candidate and existing names: looking up only the candidate's stripped form catches `some-tool` versus `sometool`, but misses the reverse direction when the existing package contains punctuation. Treat this as registry-policy eligibility evidence, not a guarantee that registration would otherwise succeed. When a candidate name is already registered, distinguish the target's own -organization from an unrelated party before calling it a clash. `npm owner ls -` and the package's repository/homepage metadata usually settle it. +organization from an unrelated party before calling it a clash. Correlate `npm owner ls `, version-level publisher metadata, known target-controlled npm organizations, and independently verified repository provenance. Repository/homepage fields are self-asserted supporting evidence and do not settle ownership alone. If publisher identity remains ambiguous, mark it `UNKNOWN`. ## Validation and Impact @@ -178,27 +193,15 @@ Do not report an unregistered name without an execution path, or an execution pa ## Reporting -There is no CVE and no vulnerable version here, so this does not go through -`create_dependency_report` — that tool is for advisory-matched dependency -versions and requires a CVE. File a proven case with -`create_vulnerability_report`, using the non-destructive resolution transcript -as the PoC. Never publish, reserve, or install a contested name as evidence. +There is no CVE and no vulnerable installed version here, so this does not go through `create_dependency_report`; that tool requires an advisory-matched CVE. Use `create_vulnerability_report` only after the applicable core condition is fully verified. -Gate severity on the execution context and the completeness of the chain: +A registry lookup or `404` alone is candidate evidence, not a working PoC. The report must preserve the target invocation and execution context, show the exact selected package and binary/module, demonstrate the runner's execution transition in a representative controlled setup without publishing the contested name, and establish the authority inherited by that process. When source is available, include the responsible invocation/configuration and concrete fix in `code_locations`. -- **High/critical** — the fallback is proven in a privileged context (CI, - release/publish pipeline, container build, or an agent/MCP launcher running - with real credentials), the name is unowned by the target and claimable, and - no `--no`, version pin, scope routing, or local install prevents the fetch. -- **Medium** — the name resolves to an unrelated third party, or the fallback is - proven in a developer-local context, but full exploitability is not - established. -- **Low/informational** — documentation- or comment-only references, contexts - where the command is locally satisfied, names that are unregistered but - unregisterable, or a resolution chain that stops before execution. +Do not file documentation/comment-only references, locally satisfied commands, unregisterable names, ambiguous ownership, or chains that stop before package execution. Retain them as investigation notes only when useful. -Deduplicate by candidate name plus evidence path; one report per distinct name, -not per call site. +Derive CVSS from the demonstrated path rather than a fixed severity label. Account for required developer/user action, registry and configuration prerequisites, runner permissions, credential availability, and the confidentiality, integrity, and availability actually exposed. A CI, release, container-build, or agent context can be severe, but the context name alone does not establish High or Critical impact. + +Deduplicate by root cause, affected asset/workflow, and remediation. Combine call sites when the same configuration mistake and fix apply; keep separate findings when the same candidate name affects different products, tenants, runner semantics, authority, or fixes. ## False Positives @@ -208,21 +211,23 @@ not per call site. - The public package is the deliberately selected third-party tool. - npm fetches the manifest but cannot infer or execute a bin. - The reference appears only in generated/minified text with no executable call site. -- A registry/proxy error is misread as an unregistered name, or the control pair above was never run. +- A registry/proxy error is misread as an unregistered name, or the target-path control pair is inconclusive. - A package is absent but registry policy prevents the contested registration. -- The command is popular ecosystem tooling (`tsc`, `eslint`, `prettier`, `vite`) resolving to its real maintainer; a `200` there is the intended tool, not a clash. +- The command resolves to the deliberately selected ecosystem tool and expected publisher. - The already-registered name belongs to the target's own organization. +- An explicit `dlx` or `npm:` package spec is treated as missing-local fallback without evidence of a package/publisher mismatch. ## Remediation - Install the intended package and invoke its local executable through an npm script. -- Bind the command explicitly: `npx --package @org/tool org-tool`. -- Use `--no` where a missing local dependency must fail instead of fetching, and prefer `--package @org/tool` in any privileged workflow. -- Reserve the unscoped `bin` names of published scoped packages so the fallback name cannot be taken by a third party. +- For npm, bind and pin the provider: `npx --package @org/tool@ org-tool`; use `--no` when a missing dependency must fail. +- For Bun, use `bunx --package @org/tool@ org-tool` and `--no-install` when remote installation is not intended. +- Replace `yarn dlx`/`pnpm dlx` in repeatable or privileged workflows with a declared, locked dependency plus the runner's local `exec` command. When ephemeral execution is required, bind and pin the provider package explicitly. +- For Deno, pin the `npm:` package and binary subpath, retain a reviewed lockfile, use cache-only operation where appropriate, and grant only the permissions the command requires. - Route private scopes to the intended registry and prevent public fallback. - Pin package versions and lockfiles in privileged workflows. - Replace bare `npx -y ` agent launchers with reviewed, publisher-qualified, version-pinned package specs. ## Summary -Treat npx confusion as an execution-context bug: an unresolved executable is reinterpreted as a package name and fetched from a registry. Prove each resolver transition, distinguish binary names from package names, and evaluate every working directory and automation context independently. +Treat package-runner confusion as an identity and execution-context bug. Prove the runner-specific transition, distinguish binary names from package names, verify registry and publisher state without equating absence with eligibility, and report only a complete execution path under the affected workflow's actual authority.