Your npm install was safe. What happens when the package runs?
InfoWorld ·

For years, one of the first questions we asked about a suspicious npm package was whether it ran a pre-install or postinstall script. Those hooks can execute before a developer has written a line of code that uses the dependency. Blocking them is a sensible defense. It prevents a particular kind of package from running just because someone installed it. But installation is only the beginning of a package’s life in an application. Once that application starts, it calls the package’s functions with its own permissions, environment variables, network access, and data. A package can behave impeccably during installation and still become dangerous when the application uses it. That distinction is the lesson of the recently disclosed indexed-btree campaign . Checkmarx Zero found a malicious npm package that imitated the legitimate sorted-btree library. The package had no malicious install hook. Its loader sat inside BTree.prototype.set , a method the application would call as part of normal use. The reported trigger checked for a particular key value before launching an obfuscated file in a separate Node process. Merely installing the package did not trigger that behavior; neither would an import alone necessarily do so. The application had to reach the relevant method and condition. The precise key and second-stage machinery are useful to incident responders. The larger design failure is simpler: an install-time policy can tell us whether a package executed during installation. It cannot tell us whether code inside that package is safe to execute later. The wrong boundary When a team turns off life-cycle scripts, its installation process becomes safer in a specific, measurable way. npm’s ignore-scripts setting prevents scripts declared in package.json from running during installation. npm also offers controls for approving install-time scripts. Neither mechanism claims to inspect every branch of a library’s runtime code. We sometimes talk as though a quiet install means a clean dependency. That is a category error. A package is executable code regardless of whether package.json contains a "scripts" field. The application may load it on startup, call it in a request handler, or run it in a build tool with access to CI (continuous integration) credentials. The attacker gets to choose which moment is useful. There are at least three moments to consider: installation, module loading, and ordinary function calls. They carry different risks and require different tests. Some malicious packages execute at module load. Others wait for a function call, a particular argument, an environment variable, or a later point in the application’s life. The indexed-btree package belongs in that last group. A scan that checks only install hooks is looking at one moment and treating it as the whole story. This matters because the runtime context can be more valuable than the installation context. A developer’s laptop may have cloud credentials. A CI job may have a release token. A production service may have access to customer records or internal APIs. The exact access depends on how each environment is configured, but the package generally runs with the privileges available to the process that calls it. If the process can read a secret or open a network connection, untrusted dependency code may be able to do the same. Why a normal-looking package can be difficult to judge A dependency often has to look and act enough like the expected library to survive ordinary testing. A copied API, plausible repository, and working happy path can make a substitution seem routine. Developers review the behavior they need, while malicious behavior can sit on an uncommon path or behind a condition that the project’s tests never reach. The indexed-btree example is particularly instructive because the trigger was placed in a legitimate-looking data structure operation. Calling set on a B-tree is not suspicious. A test suite might call it repeatedly and still miss a condition tied to a specific key. Even if the condition is reached, a detached child process and swallowed errors can leave the calling application looking healthy. That is an observation about the reported code path, not proof that every installation led to compromise. This is also why download counts, a polished README, and a passing install do not establish trust. Download figures describe distribution, not the safety of the code in a specific version. A package name that resembles a known project deserves closer scrutiny, particularly when a new dependency appears in a pull request or a lockfile change unexpectedly introduces a different package. The question for a security review should be concrete: What code did this change bring into our application, where does it run, and what can that process access? That question is harder than checking for a life-cycle script, but it matches the way the dependency actually operates. An ordinary vulnerability scan may not answer it either. A newly published malicious package need not have a CVE or a known vulnerable version for an advisory database to report. A gate for new dependencies The practical answer is to make dependency changes a separate, enforceable part of code review. A pull request that adds a package or changes its resolved version should produce a dependency diff before it can merge. Known-vulnerability checks remain useful, but they are only one input: indexed-btree did not need a known CVE to be dangerous. The gate must also show the exact package name, version, provenance when available, and the published files that will actually run. For an existing dependency receiving a routine update, teams can apply their normal policy. A new name, a switch to a look-alike package, a change of publisher, or an unexplained executable or obfuscated file should pause the merge for an owner to investigate. That is a decision rule, not a request for every engineer to read every line of node_modules . Review the resolved artifact and its change from the previously approved version; record why the package is needed and where it will execute. Use an automated dependency diff to make this visible on every pull request, then require the check in branch protection. GitHub’s dependency review, for example, can enforce checks for newly introduced known vulnerabilities and show package changes. It does not certify that a package without an advisory is safe. Add a separate rule for new or unexpected package identities and route those cases to a designated reviewer. The merge outcome should be explicit: approve the exact version for a defined use, investigate it, or reject it. Test behavior and limit its reach When the review raises a question, run the published artifact in a disposable test environment with no production secrets. Exercise the API calls and inputs the application will actually use. Watch for child processes, unexpected file reads, and outbound requests, and fail the test on behavior the library does not need. In the case of indexed-btree, an import-only test could miss the conditional call inside BTree.prototype.set . A passing test covers the paths exercised; it cannot prove that every hidden condition was reached. The stronger protection applies even when a malicious path escapes testing: give the process fewer capabilities. Build jobs should not inherit release credentials unless they are publishing. Production services should receive only the secrets and network destinations they require. Use operating system or container isolation to restrict filesystem access, process creation, and egress where the workload allows it. These restrictions need to be tested against the real application so teams know which paths are intentionally permitted. For a team adopting this tomorrow, the minimum policy is clear. Make the lockfile and dependency diff part of every pull request. Require an owner for a new package identity or a surprising published artifact. Run uncertain packages through realistic calls in an isolated job. Keep high-value credentials out of the processes that use third-party code. None of these steps alone detects every malicious package; together they prevent a clean install from becoming an unearned approval. A more honest security claim The phrase “we block install scripts” is valuable when it means exactly that. Trouble begins when it becomes shorthand for “dependencies cannot execute malicious code.” A malicious library does not need an installation hook if its normal API gives it a route into a privileged process. The next generation of supply-chain defenses will have to follow dependencies past the install command. That means treating package adoption as a decision about executable behavior, not simply a decision about whether package.json contains a script. The malicious indexed-btree package is a timely example, but the underlying problem will remain after this campaign is over: the application eventually has to run the code it chose to trust.