fix(npm): use the dev-build fallback only from a source checkout - #1096
Conversation
There was a problem hiding this comment.
The guard is correct for the layout it targets and is strictly better than what is on main: __dirname/../.. is the repo root from a checkout and node_modules from an install, and only the first has Cargo.toml.
Nit: the claim that Cargo.toml cannot appear under node_modules does not hold on a case-insensitive filesystem. cargo.toml is a legal npm package name (lowercase, dots allowed, cf. socket.io), npm unpacks it as the directory node_modules/cargo.toml/, and existsSync is true for directories, so on macOS and Windows the marker check passes. The same attacker now needs two planted packages (target and cargo.toml) instead of one, so the bar is raised rather than the case closed.
Requiring a regular file removes it, since npm can only ever create a directory at that name:
import { existsSync, statSync } from "fs";
// ...
const inSourceCheckout =
statSync(join(repoRoot, "Cargo.toml"), { throwIfNoEntry: false })?.isFile() === true;throwIfNoEntry needs Node 14.17+, and engines.node here is >=18. Not sent as a suggestion block because it also touches the fs import on line 3.
No test coverage is a fair call at this size; if getBinaryPath grows another branch, a node --test file that stubs the filesystem would be worth it then.
## [7.21.7] - 2026-09-20 ### Bug Fixes - fix(npm): use the dev-build fallback only from a source checkout (#1096)
Closes #1097.
The npm wrapper's dev-build fallback resolves outside the package when installed, into a directory any third party can own. Found while checking a scanner alert on
npm/bin/ferrflow.js, which was itself a false alarm.The path
From a source checkout
__dirnameis<repo>/npm/bin, so this is<repo>/target/release/, the Cargo output. That is the intent and it is fine.Installed from npm,
__dirnameisnode_modules/ferrflow/bin, and the same expression is:which belongs to whatever package installs as
target.targetis a real package on npm (0.0.2, harmless today).Reproduced
The wrapper ran a binary from
node_modules/target/. On a supported platform, with the real one simply not installed.Why it is worth fixing rather than noting
Optional dependency installs fail silently by design, so the fallback is reachable without the user doing anything unusual: a network blip during
npm installis enough to leave the platform package absent and the wrapper looking atnode_modules/target/.The sharper point is that this package has no install scripts, which is a property people check for and rely on. This path quietly gives that guarantee away: a hostile package needs no
postinstall, only a file atrelease/ferrflowand a user who eventually typesferrflow.It is not a high-severity hole. It needs a package named
targetin the tree, and anyone who can put one there has other options. It is a one-line guard against a case that should never have been reachable.The fix
Use the dev build only when the parent of
npm/is actually the FerrFlow source tree:Cargo.tomlis the marker because it cannot appear where the bad case lives: npm unpacks a package's files undernode_modules/<name>/, so nothing can createnode_modules/Cargo.toml, and the name is not a legal npm package name anyway (uppercase).Verified both directions
Installed shape, with the planted binary still in place:
Source checkout,
Cargo.tomlpresent andtarget/release/ferrflow.exebuilt:And with
Cargo.tomlrenamed away, the same checkout refuses, which is the node_modules shape.Tests
None added. There is no Node test runner in this repo and no CI step that would run one, so a test here means introducing a harness for a three-line function. The two runs above are the verification. Happy to add
node --testcoverage forgetBinaryPathas its own change if the wrapper is going to keep growing logic.Not changed
The two files the scanner flagged are fine.
npm/bin/ferrflow.jsspawns with an argv array and noshell: true, which is correct for a launcher, andscripts/validate-site-docs.mjshas nochild_processat all (its onlyexeccalls areRegExp.exec) and is not even published: the npm tarball is four files.