This article is originally available at Snyk.io.
Malicious npm packages and their dangers have been a frequent topic of discussion — whether it’s hundreds of command-and-control Cobalt Strike malware packages, typosquatting, or general malware published to the npm registry (including PyPI and others). To help developers and maintainers defend against these security risks, Snyk published a guide to npm security best practices.
All that said, the following attack scope, which Yagiz Nizipli alerted long-time maintainers to, and the real-world risk related to data compromise are a great example of how important it is to minimize the risks of arbitrary command execution with package managers, such as those employed via npm’s postinstall lifecycle hooks.
Life cycle scripts of npm
Node Package Manager (npm) provides a set of scripts for developers and package maintainers to
maintain the life cycle events of a package. These scripts provide significant value to
developers by enabling them to perform various tasks or configurations as part of the package
installation process. For example, with
postinstall scripts, developers can automate tasks such
as building assets, setting up environment variables, running migrations, or any tasks that can
be automatically executed.
scripts property of a
package.json file defines the commands triggered by the package's
lifecycle and the dependent of the package you're developing. As of today,
a limited number of life cycle scripts in any scripts property of a package.json file.
For simplicity, the rest of this article will focus on the
postinstall command. However, all concepts provided by this article also apply to other life cycle operations.
Past security incidents
- The cross-env security incident discovered by Oscar Bolmsten.
- The eslint-scope security compromise.
- The event-stream spear-headed attack on cryptocurrency application developers.
Security professionals identify the protection of assets when the data is stored or at rest by
Data-at-rest, as opposed to when it is in transit or being processed. It focuses on protecting
the sensitive information stored in databases, file systems, or persistent storage. Data-at-rest
security aims to prevent unauthorized access, disclosure, or data tampering while it is dormant.
Various measures are available to ensure data-at-rest security, such as:
- On-demand decryption: Decrypting only the data required to perform the current task and storing the rest of the data encrypted to prevent forbidden access.
- Access control logic: Validating the requester's identity through a mechanism such as a password, two-factor authentication, or biometrics provided by an operating system (such as FaceID) — making it possible to limit the exposure of the resource to unwanted people.
Attack surface of a developer
Industry best practices force us to use and follow principles to develop applications. These best practices offer many advantages when working with different teams and developers but also increase the attack surface.
What sort of data is lying around unencrypted in a developer machine?
- Environment variables through plain text files, such as .env (available for consumption through the dotenv package).
- Configuration files for projects stored as a JSON file, such as config.json.
- SSH keys for accessing Github/Gitlab, which are available in the ~/.ssh folder.
- And... macOS Keyboard Shortcuts!
macOS Text Replacements
macOS, by default, has a feature called
Text Replacements hidden inside the system preferences
applications. This feature allows users to quickly replace a word with another word. Just
recently, I've learned that a developer from a well-known company was using text replacements
@card keyword with their credit card information. Even though the credit card number
without the expiration date or CVV does not expose your money to outsiders, it adds an attack
surface for them to exploit.
Text Replacements feature is available through System Preferences application, under the
Exfiltrating keyboard text replacements
Keyboard shortcuts are stored under
defaults, which corresponds to a filesystem backed
file somewhere in your local folder. Executing the following command will return your configured
text replacements, which are also available through the System Preferences application.
Remember that the following code does not require
sudo access and can be executed by any
process in your computer.
The same command can be executed through
execSync in Node.js, and parsed without any hassle,
postinstall life cycle operation supported by the
npm package manager.
The following is an example of a Node.js script that can be employed by malicious actors to access macOS text replacements and exfiltrate sensitive data:
To make sure the above code runs when this package is installed, we will update the package
manifest file as follows
When distributed through npm, and downloaded by a developer, this library will directly execute our custom script to retrieve and process the keyboard replacements. If you aren’t careful, it's easy to miss the line containing > node ./retrieve.js.
What can you do as a developer to mitigate the security risks of malicious npm packages and general security concerns of arbitrary command execution from packages in your dependency tree?
Ignore scripts on npm package installations
Protecting yourself from packages that leverage
postinstall scripts is possible. npm
--ignore-scripts configuration when installing packages.
Use safe npm defaults
NPM has a configuration file called .npmrc. You can change the default
preferences using the
npm CLI to ensure secure defaults:
Most importantly, you should never store sensitive information in plain text. If you have to store it in plain text due to other requirements, you should always make the resource accessible through multi-factor authentication.
Note: Originally, I've written this article on Snyk blog published by Liran Tal.