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
[…]
Content was trimmed to protect the source. Please visit the original article for the full text.
Read the original article:
Your npm install was safe. What happens when the package runs?
