Skip to content

Latest commit

 

History

History
144 lines (100 loc) · 7.51 KB

File metadata and controls

144 lines (100 loc) · 7.51 KB

OA006: Coupled platform binary

Severity: high (platform binaries) / medium (other targets)  ·  Action: replace (multi-op: remove binary override + add parent override)

Severity tiers

Condition Severity
Target matches a platform-binary pattern (@esbuild/<platform>, @next/swc-<platform>, @img/sharp-<platform>, lightningcss-<platform>, etc.) high (binary-coupling failure mode is severe)
Non-platform target (e.g. postcss, react), override not confirmed effective on disk medium (the override may not be taking)
Non-platform target AND OA008 also fires for the same target high (escalated in the composite pass: confirmed failure on disk)

Platform-binary detection is heuristic: any package whose name contains an OS segment (linux, darwin, win32, freebsd, android, etc.) as a hyphen- or slash-separated token. See src/overrides/detectors/platform-binary.ts.

What it catches

An override whose target package has at least one installed parent that declares it as an exact-version dependency (not a range). The override is fighting the parent's pin. Depending on the package and the npm/pnpm version, the override may:

  • Be honoured but emit EOVERRIDE warnings.
  • Be silently ignored, leaving the parent's pinned version on disk.
  • Cause both versions to install side-by-side (defeating dedup, leaving the vulnerable copy on disk).

This was a headline finding from dogfooding against hexmetrics. It surfaced a class of override-hygiene bugs that no other tool catches.

Materialization gate (calibration)

OA006 consults the materialized node_modules tree before firing, the same way OA008 does. A flat overrides / pnpm.overrides / resolutions entry does collapse a transitively exact-pinned dependency in npm and pnpm - that is its entire purpose. So if the override target is installed at a version that satisfies the override, the override demonstrably won and OA006 stays silent: it is effective, not fragile, and proposing the parent-level rewrite would be harmful (it would force-pin the parent to clear a non-problem).

OA006 therefore fires only when the override is not confirmed effective on disk:

  • a materialized copy violates the override (the parent's exact pin won resolution), or
  • there is no materialized copy to prove the override took (e.g. a wrong-platform optional binary that was never installed).

The classic false positive this rule must avoid is the documented "every Next.js project needs a flat postcss override" pattern: next exact-pins postcss, but the flat override wins and node_modules/postcss ends up at the override's version. That override is working; OA006 must not cry wolf on it. (Calibrated per issue #37 after a HexOps fleet dogfooding sweep flagged it on 32 of 33 Next projects.)

Example

// package.json
{
  "overrides": {
    "@esbuild/linux-x64": "latest"   // <- targets a platform binary
  }
}

Installed tree:

node_modules/esbuild@0.28.0
  optionalDependencies: { "@esbuild/linux-x64": "0.28.0" }   <- EXACT pin
node_modules/@esbuild/linux-x64@0.25.12                        <- frozen by lockfile

Output:

HIGH (1)
--------
  OA006  @esbuild/linux-x64
    package.json/overrides/@esbuild~1linux-x64
    Override on platform binary fights an exact-pinned parent
    fix: applyable patch (2 ops)

The finding details explain the parent-level rewrite:

// instead of:
"overrides": { "@esbuild/linux-x64": "latest" }
// do this:
"overrides": { "esbuild": ">=0.28.0" }
// then: rm -rf node_modules package-lock.json && npm install

Why it matters

Platform binaries (@esbuild/<platform>, @next/swc-<platform>, @rollup/rollup-<platform>, @swc/core-<platform>, sharp prebuilts, lightningcss-darwin/linux/win32, etc.) are typically declared by their parent JS package as exact-version optionalDependencies. The version of the native binary must match the JS package's expectations byte-for-byte; a mismatch can crash at startup.

When you override just the binary without the parent:

  1. The override looks load-bearing: "we pinned the binary to the safe version".
  2. The actual install behaviour depends on npm/pnpm version, registry state, and the parent's manifest. Sometimes the override wins, sometimes the parent does.
  3. Even when the override wins today, a future npm update to the parent will silently re-introduce the parent's exact pin and undo your security work.

Static analysis of package.json alone misses this entirely: pnpm audit and most override readers look at version strings, not at the parent's declared constraints. OA006 cross-references the override target with the installed tree's parent manifests and surfaces the coupling.

Common targets where this bites

Override target Likely parent Why it's coupled
@esbuild/<platform> esbuild Native bundler binary; version-locked to JS parent
@next/swc-<platform> next Next.js compiler binary
@rollup/rollup-<platform> rollup Native rollup binary
@swc/core-<platform> @swc/core SWC compiler binary
lightningcss-<platform> lightningcss CSS engine binary
sharp prebuilts sharp Image processing binary

How to fix

cve-lite overrides --fix --rule OA006

--fix applies the structural rewrite automatically as a multi-op patch: it removes the binary override entry and adds (or replaces) a parent override entry at >=<parent-installed-version>.

General pattern:

// remove the binary override:
"overrides": {
  // "@parent/<platform>": "..."      <- delete this
  "parent": ">=<safe-version>"       // <- add this
}

Then flush the lockfile to pick up the new resolution:

rm -rf node_modules package-lock.json
npm install
cve-lite overrides                    # confirm OA006 + any OA008 are cleared

Why a floor (>=X.Y.Z) and not exact? Floors let the resolver pick newer-but-compatible versions on future installs, which is what you want for a security pin.

How OA006 picks the suggested parent

When multiple installed parents declare the override target exact, the detector picks the newest one (sorted descending by semver) as the suggested override target. That's the safer recommendation; newer parents likely have the safer binary too. The choice is deterministic across runs.

Complementary rules

  • OA006 flags the risky pattern: your override is fighting an exact-pinned parent.
  • OA008 flags the materialized failure: a vulnerable version is currently on disk despite the override. The two together tell you whether the risk has actually bitten you yet. When both fire on the same target, the composite pass escalates a medium OA006 to high.

When you might want to keep the binary-level override

Rarely. The one legitimate case: you've forked the platform binary (your CI builds a patched binary you publish under your scope), and you genuinely want the override to substitute just the native artifact. Even then, prefer pinning the parent to a known-compatible version and using a custom registry mirror.

References