DEV Community

Anwar Nairi
Anwar Nairi

Posted on

Lock your dependency to prevent supply-chain attacks

If you followed recent news, a new supply-chain attack affected recent versions of several TanStack packages.

This article proposes a simple approach to reduce the likelihood and impact of this kind of security breach in your applications.

Summary


What is a supply-chain attack?

A software package depends on multiple elements throughout its lifecycle:

Developer -> Computer -> GitHub Repository -> Package Registry (NPM)

A supply-chain attack happens when one of these elements is compromised, allowing malicious code to propagate through the rest of the chain.

In practice, this often means an attacker manages to inject malicious code into a package that many other projects depend on.

What happened?

An attacker submitted a malicious pull request targeting one of the TanStack packages.

The pull request contained hidden malicious JavaScript code. During the CI process, a GitHub Action workflow executed and the attack poisoned the GitHub Actions cache.

The malicious code was then able to capture sensitive credentials, including GitHub tokens.

Using those credentials, the attacker gained elevated permissions and published compromised package versions to NPM.

As a result, developers installing the affected versions unknowingly downloaded malicious code into their projects.

Why did it affect other projects

By default, Node.js projects usually allow dependency updates within the same major version.

For example:

npm install solid-js
Enter fullscreen mode Exit fullscreen mode

This produces the following entry in package.json:

{
  "dependencies": {
    "solid-js": "^1.9.5"
  }
}
Enter fullscreen mode Exit fullscreen mode

The ^ character means:

Allow future minor and patch updates automatically.

So even if your project originally used 1.9.5, reinstalling dependencies later may install a newer version such as 1.11.2.

This can happen in many situations:

  • A new developer joins the team and installs dependencies
  • A CI/CD pipeline runs npm install
  • A GitHub Action executes automated tests
  • You reinstall dependencies after deleting node_modules
  • You run npm update

If a malicious version is published during that time window, your project may automatically fetch it.

How version locking reduces risks

The goal is simple:

If version X.Y.Z worked yesterday, install that exact same version tomorrow.

NPM provides a --save-exact flag for this purpose:

npm install --save-exact solid-js
Enter fullscreen mode Exit fullscreen mode

This produces:

{
  "dependencies": {
    "solid-js": "1.9.5"
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice that the ^ is gone.

Now every npm install will use the exact same version unless you explicitly change it yourself.

You can also lock a specific version manually:

npm install --save-exact solid-js@1.8.0 
Enter fullscreen mode Exit fullscreen mode

This approach exists in many ecosystems.

PHP

composer require laravel/reverb:1.10.1
Enter fullscreen mode Exit fullscreen mode

Ruby

gem install sidekiq -v 8.1.4
Enter fullscreen mode Exit fullscreen mode

Python

poetry add requests@2.34.0
Enter fullscreen mode Exit fullscreen mode

Limitations to this solution

As always in software engineering, there is no free lunch.

This approach comes with several trade-offs:

  • You must review and update dependencies manually
  • You should run npm outdated regularly to stay up to date on security fixes
  • Locking direct dependencies does not fully protect you from compromised transitive dependencies
  • Security vulnerabilities may remain unnoticed longer if updates are delayed

In other words, version locking reduces the attack surface, but it does not eliminate supply-chain risks entirely.

Conclusion

Supply-chain attacks require active monitoring and good security practices.

Exact version locking is not a silver bullet, but it helps reduce unpredictability and limits the risk of accidentally installing newly compromised package versions.

An additional benefit is build reproducibility:

  • team members use the same dependency versions
  • CI environments become more predictable
  • debugging becomes easier
  • unexpected dependency regressions are reduced

However, this approach also increases maintenance overhead because dependency upgrades must be reviewed more carefully.

For some teams, this trade-off is worth it. For others, it may feel too restrictive.

The important part is understanding the risks and making an intentional decision rather than relying on default package manager behavior.

Top comments (0)