NPM security: preventing supply chain attacks
2022年11月8日
0 分で読めますNPM security has been a trending topic in the media in recent years, mostly in reference to npm packages available on the ecosystem rather than the npm registry itself. The increasing security risk, that applies to developers and software we build, makes it even more important to understand how to prevent supply chain attacks and other security vulnerabilities related to software development life cycle.
Supply chain attacks have taken many forms — such as dependency confusion attacks, spearheading malicious code backdoors in open source packages, and compromising build pipeline infrastructure. The security risks prevalent in open source libraries and ecosystems pose an imminent threat to developers.
What are some software security controls we can apply for a better security posture? In this post, I intend to unveil npm security practices and supply chain security tooling available for you as a JavaScript developer (TypeScript developers are welcome too).
The current state of supply chain attacks in 2022
We’re seeing more evidence of how developers play a key role in application security. Developers are fundamentally rooted in growing security incidents, such as the peacenotwar module, the dependency confusion attack against gmx-reference, and the Cobalt Strike dependency confusion attacks — which are just several examples of recent npm security headlines.
How many npm dependencies and different maintainers do you estimate you have in your projects? An academic research published in June 7th 2019 shared interesting insights about this topic:
Installing an average npm package introduces an implicit trust on 79 third-party packages and 39 maintainers, creating a surprisingly large attack surface.
Threat actors who focus their effort on supply chain attacks always find innovative ways to penetrate an ecosystem and position it with malware. One prime example of this is a recent security research article, published in 2021, that found:
2818 maintainer email addresses associated with expired domains, allowing an attacker to hijack 8,494 packages by taking over the npm accounts. […] We found 58.7% of packages and 44.3% of maintainers are inactive in the npm registry.
Despite these concerns, open source software is a great contributor to many innovative technologies and helps organizations with speeding up business deliverables. As big of a gift as open source software is to the world, it’s still important to call attention to the risks involved with third-party software libraries and their blast radius when bundled in our applications.
What is supply chain security?
It is quite common for developers to think about open source supply chain security in terms of immediate touch points such as the npm packages that we install (npm install event-stream
) and import to our projects (import colors
). However, the software supply chain attack surface is actually much broader than that.
What is a supply chain attack?
In the context of software development, a supply chain attack manifests in the form of threat actors injecting their malware, backdoors, or other form of attack payloads into software components or software-related infrastructure, that is then used to produce other working software. In other words, threat actors do not directly exploit or research vulnerabilities in the source code of the targeted software.
The supply chain levels for software artifacts, also known as SLSA for short, provides a good reference to weak integration points where risks await. The following is a visual representation of that borrowed from Snyk’s Supply Chain Security White Paper:
If we return to the basics of how we build software today, you can see many of these familiar milestones for producing software actually have a very real threat landscape impacting them. In fact, the following article on Google’s security blog provides multiple examples of real-world security incidents for each of these software delivery milestones.
NPM security and preventive measures for supply chain security
(1) Prevent NPM lockfile injection
In September 2019 I disclosed my security research that outlined the inherent security risks with package lockfiles in the npm ecosystem. Both JavaScript package managers, Yarn and npm, were found to be susceptible.
The security threat takes place with malicious actors gain the access and ability to contribute source code changes, via mechanisms such as pull requests, commonly executed on GitHub as a way to contribute to open source projects. In that context, if they were to make updates to a lockfile such as package-lock.json
or yarn.lock
, to include a new npm package dependency, or even just modifying the source URL of an existing package, then any invocation of package install (such as npm install
or yarn install
) would fetch said malware.
The following screenshot shows how I injected a malicious package through a pull request I made on an open source package on npm. Take a closer look and see if you can spot it:
You might be running through the following check-list:
The packages I introduced are known in the ecosystem and are vulnerabilities free – check.
There are no typosquatting attempts in these package names – check.
These are valid versions of those packages and aren’t malicious in and of themselves – check.
Yet you’d still be out of luck finding it.
Try expanding the yarn.lock
file. A package manager’s lockfile is machine generated and was not meant to be readable or maintainable by the human eye. This means that it makes things, like the following malicious update that I injected into it, very hard to find:
Furthermore, JavaScript package managers allow users to install packages from very peculiar sources, such as a GitHub’s gist, or directly from a source code repository.
This means I can simply update the lockfile to specify a new source location (such as in the resolved
key) that I, as the attacker, have full control of. I can also set the SHA512 integrity value accordingly so that no alarms are thrown off.
If this pull request is merged, then the next collaborator on the project who runs a yarn install
effectively gets my malicious package. But actually, you don’t even have to wait for a developer to interact with the project — because many mature open source projects have a Continuous Integration (CI) environment.
It’s very likely that, upon solely creating this pull request, an automated build that runs on GitHub Actions (or maybe CircleCI) will begin. When it builds this pull request, it will fire off the npm install process, from which I can potentially access and steal sensitive information such as your CI environment secrets, API keys, etc.
To prevent this type of lockfile injection attack, you can use lockfile-lint. Using lockfile-lint
, developers can lint their lockfiles and ensure that these machine generated snapshots of the pinned dependencies are upholding a specific trust policy.
For example, you can tell lockfile-lint
to fail if it finds that the yarn.lock
lockfile includes sources of npm packages to be installed from anything that is not the official yarnpkg.org
mirror:
To summarize the preventative measures for this potential security risk:
Use
lockfile-lint
in your development environment and your CI.Do not allow external contributors to update the lockfile.
Use automated dependency upgrade bots, which update the lockfile for you in an automated and trusted way (they fetch from the official registry). Hint: the Snyk bot does that.
(2) Prevent arbitrary command execution
Hopefully, it’s no a secret that running the npm install
command may cause the packages you install to execute arbitrary commands as part of the install process.
If you were at one specific point in time to run the following command:
1npm install @vue/cli
You would’ve been the victim of the node-ipc npm package sabotage — in which the maintainer modified their package to execute commands that will create files in your desktop folder. In other versions of the node-ipc
npm package they’ve taken a more harmful path, including one that traverses through your hard drive files and wipes them entirely.
Dependencies having the power to run arbitrary commands is possible because the npm package manager provides an install lifecycle hook as part of a dependency install process. Most of the time, it is used by module maintainers for legitimate reasons — such as preparing installation artifacts, fetching resource files, or cleaning up after an install. It is also a viable way to perform cross-compilation for native module bindings that need to be compiled in the host where they are installed to.
The former What are Weak Links in the npm Supply Chain? research we referred to, provides some insight into the use of install hooks in the ecosystem:
We found 2.2% (33,249) of packages use install scripts, indicating that 97.8% of packages may follow npm recommendation of not using the install script as best security practices.
Summarizing preventative measures you should take:
Append the
--ignore-scripts
flag to yournpm install
command so it skips the execution of arbitrary commands by npm package dependencies.Consider adding this flag to your project’s
.npmrc
configuration file so that other developers working with you on the project are protected as well.Take into account that enabling this flag may mean that some legitimate use-cases of npm packages are prohibited from running and may cause the npm install process to break.
Take note that the
--ignore-scripts
flag does not apply to packages installed from local file sources or when installing an npm package from a git repository.
(3) Avoid blind NPM package upgrades
Some developers may explicitly upgrade all of their dependencies to the latest versions, as part of a continuous integration (CI) process. They do that, along with running tests and happy path test cases, with the goal of ensuring that their applications still work as expected even when their dependencies release new versions. It’s a way to ensure forward compatibility that will not break an application.
You can upload to a latest npm version by either running:
1npm update
Or with the dedicated npm package for that:
1npx npm-check-updates -u
While reasons for that are legitimate, it also means that these so-called blind npm package upgrades are inviting potential security incidents. As we just learned, running an npm install is quite dangerous.
Blindly upgrading your dependencies poses an inherent security risk of exposing you unnecessarily to threats, such as dependency confusion attacks, and other incidents we’ve learned about in recent years, such as the colors security incident, or the node-ipc security incident.
Preventative measures you should take to keep your dependencies up to date:
Use a smart and automated dependency upgrade tool. Such tools often have an auto-update manifest and lockfiles capability built-in.
Carefully review the changelog and release artifacts. Ensure that the maintainer hasn't changed or there’s a good reason for that. Take note that the newly released version also has its source code counterpart available too.
It’s especially important to call out automated upgrades and the risk of pulling a malicious npm package. Snyk does not recommend upgrading to versions that are less than 21 days old. This is to avoid versions that introduce functional bugs and are subsequently unpublished, or versions that are released from a compromised account (where the account owner has lost control to someone with malicious intent).
Learn more about that in the Snyk docs for upgrading dependencies with automated PRs.
(4) Prevent dependency confusion
Dependency confusion is a type of attack in which a package is created and used internally for an organization, in some private proxy or hosting service, but the name of said package remains free to register on the npm registry.
Unfortunately, it’s easy to have local tooling misconfigured. That, together with the way that the npm package manager fetches packages, creates the following situation — when a package of the same name exists on the public npm registry, and a higher version exists than that which is installed, then npm will install it.
If a Microsoft employee was not careful, or their npm package manager and internal proxy server was misconfigured, the following would have resulted in a dependency confusion attack that could infiltrate internal company infrastructure:
1npm install azure-core-tracing-samples-js
In fact, the same risk applies to the case in which employees were running this trusted npm package updates command:
1npm update
The reason for that is rooted with the way dependency confusion attacks work. Even though this was disclosed as public research in 2021, the Snyk security research team has found evidence of threat actors targeting company's infrastructure with this method of attack. Furthermore, Nishant Jaint, a security researcher had outlined another potential vector of attack for dependency confusion through the use of npm package aliases.
To detect and prevent dependency confusion attacks you can use of Snyk’s free, open source tool called snync. The following is an example execution of it:
1npx snync --directory ~/my-app --private “superlaser”
2
3Testing project at ~/my-app
4Reviewing your dependencies...
5
6Checking dependency: death-star-hyper-reactor
7 -> ⚠️ vulnerable
8Checking dependency: superlaser
9 -> ❌ suspicious
(5) NPM security: Proactive protection from malware
You’ve most likely run an npm install
command to install an npm package, only to be greeted with an output such as the following:
1$ npm install amp-html
2
3added 1166 packages from 1172 contributors and audited 39128 packages in 112.505s
4found 93911 high severity vulnerability
The issue with the above is that, while there were security vulnerabilities found in the packages, you were only made aware of them after you installed those vulnerable packages. What does the amp-html
npm package hold? Before going into that, let’s explore a better way of installing packages and regaining control.
Imagine that could collect useful package health information about your dependencies prior to installing them, and then make an educated choice to either add that npm package to your project or “keep shopping” at npmjs.org.
This is where I want to introduce you to an open source project of mine called npq. With npq
, you can establish proactive security measures and make informed deicions on whether to install a package, based on signals such as whether a project has an README, a license compliance, or a GitHub repository associated with it.
The npq
tool is even fully integrated with Snyk — so if you sign up for a free account, it will pick up on the Snyk API token that is available in your environment and query the Snyk vulnerability database.
In my development environment, I aliased the npm
command to npq
as follows:
1alias npm=npq
Now, every time I run npm install
, it actually calls npq
to run its checks and validate first. If I choose to proceed, then npq will yield back to the npm package manager CLI to continue the installation:
You can also install npq
and use it as an ad-hoc tool to test vulnerabilities:
1npq install amp-html
Another way to gauge for npm package dependency health is with Snyk Advisor, which is a handy tool to search for npm packages, alternatives or similar npm packages, and gain insight into their maintenance and security status.
Here’s how the amp-html npm package would show in the Snyk Advisor:
(6) Trojan source attacks
Trojan source attacks is the name of a recently published security research article that investigated the concern of invisible source code vulnerabilities.
Here’s a snippet of code from a backend Node.js project, which handles some parts of provisioned admin access to a specific page route. It actually includes a trojan source attack. Can you spot the issue here:
1module.exports = fastify => (req, res, next, access = {}) => {
2 if (!req.query.token && access.login) { // Redirect to login
3 return fastify.login.view(req, res);
4 }
5
6 // Debug login for issue #1812
7 if (req.session.user != "user // Check if admin ") {
8 console.log("You are an admin.");
9 }
10
11 if (!req.query.token) { // Private access without token.
12 return res.code(401).send(new Error('Missing token.'));
13 }
14
15 …
I’ll turn on JavaScript syntax highlighting in my IDE to make it easier for you, so here’s a screenshot of the above snippet:
Focus your attention to the second conditional relating to the Debug login for issue #1812
. To better understand what is going on there, let’s migrate that code to a runnable Node.js code:
1#!/usr/bin/env node
2
3const accessLevel = "user";
4
5 // Debug login for issue #1812
6 if (accessLevel != "user // Check if admin ") {
7 console.log("You are an admin.");
8 }
If you were to run this code with node test.js
you would get the following output printed:
1You are an admin.
But, why? Scratching your head already I recon.
The underlying reason is that the text that makes up the above source code snippet includes control characters — which are hidden in plain sight, but profoundly change the actual source code text. These characters effectively change the order of the text from left to right and right to left. This way, they create a different semantic when an interpreter or compiler reads through it than that of the human eye which reads it unnoticed.
How would developers spot this type of issue and not miss it code reviews when code is contributed to their project?
If you’re using Snyk Code to monitor your source code, it will report when it detects security vulnerabilities such as SQL injection issues or insecure code injections. Additionally, Snyk Code will also report on trojan source related issues in code:
Snyk will also provide you a quick visual of the trojan source vulnerability as part of the rest of the vulnerabilities it finds for your project in the project overview page:
Luckily, Visual Studio Code and the GitHub platform have both been updated with visual cues that will alert you when you’re viewing source code files with invisible control characters in them.
Another open source tooling that helps to detect and mitigate Trojan Source attacks in JavaScript code bases with ESLint is the ESLint plugin, eslint-plugin-anti-trojan-source.
NPM security: What’s next?
Software supply chain security risks are ever-increasing, and if this wasn’t worrying enough, attacks have been sharpened to target developers and their ecosystems.
I highly recommend a follow-up read of these articles to better educate yourself on npm security best practices and related topics:
Preventing malicious packages and supply chain attacks with Snyk by Daniel Berman.
What is a backdoor? Let’s build one with Node.js by Ulises Gascón
10 npm security best practices by Liran Tal and Juan Picado.
If you are looking to educate yourself on secure coding practices, I also highly recommend taking one of the hands-on lessons on JavaScript security over at Snyk Learn. They’re both short and totally free!
Can NPM packages be malicious?
The unfortunate truth is that npm packages can indeed be malicious and use different techniques such as typosquatting attacks or social engineering to backdoor event-stream, to introduce malicious code that runs on the developer environment. In fact, some sources of malicious packages were attributed to maintainers sabotaging their own npm packages with malicious code like the case of node-ipc.
How can you check the security of NPM packages?
Snyk regularly finds and reports malicious npm packages, such as the discovery of more than 200 malicious npm packages. To check the security of npm packages you can use the Snyk Advisor to assess open source package health, or use (the free) Snyk CLI and repository integration to scan and monitor for malicious packages.
What can NPM vulnerabilities do?
Security vulnerabilities found in npm packages can impact your application in a way that exposes you to considerable risk. If you’re using an unpatched version of the websocket-extensions
npm package, then you are vulnerable to regular expression denial of service attacks. Similarly, if you’re using an unpatched version of the st
npm package for hosting and serving static files with your Node.js application, then you’re vulnerable to directory traversal attacks.