├── action.yml ├── action.js ├── setup-test.js ├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── index.js ├── package.json ├── LICENSE ├── ascii.js ├── test.js ├── .gitignore ├── eol-versions.js ├── README.md └── is-vulnerable.js /action.yml: -------------------------------------------------------------------------------- 1 | name: 'is-my-node-vulnerable' 2 | description: 'checks if your Node.js installation is vulnerable to known security vulnerabilities' 3 | author: 'RafaelGSS' 4 | 5 | runs: 6 | using: 'node20' 7 | main: 'dist/index.js' 8 | 9 | inputs: 10 | node-version: 11 | description: 'Node.js version to check' 12 | required: true 13 | default: 'v20.x' 14 | platform: 15 | description: 'Platform to check' 16 | required: false 17 | 18 | # https://actions-cool.github.io/github-action-branding/ 19 | branding: 20 | icon: 'flag' 21 | color: 'red' 22 | -------------------------------------------------------------------------------- /action.js: -------------------------------------------------------------------------------- 1 | const core = require('@actions/core') 2 | const { isNodeVulnerable } = require('./index') 3 | 4 | async function run () { 5 | // Inputs 6 | const nodeVersion = core.getInput('node-version', { required: true }) 7 | const platform = core.getInput('platform', { required: false }) 8 | 9 | core.info(`Checking Node.js version ${nodeVersion} with platform ${platform}...`) 10 | const isVulnerable = await isNodeVulnerable(nodeVersion, platform) 11 | if (isVulnerable) { 12 | core.setFailed(`Node.js version ${nodeVersion} is vulnerable. Please upgrade!`) 13 | } else { 14 | core.info(`Node.js version ${nodeVersion} is OK!`) 15 | } 16 | } 17 | 18 | run() 19 | -------------------------------------------------------------------------------- /setup-test.js: -------------------------------------------------------------------------------- 1 | const isOldEnough = require('./eol-versions') 2 | const assert = require('assert') 3 | 4 | // When old enough an error is thrown 5 | if (isOldEnough(process.version)) { 6 | runCompatibilityTest() 7 | } else { 8 | require('./test') 9 | } 10 | 11 | function runCompatibilityTest () { 12 | const childProcess = require('child_process') 13 | const path = require('path') 14 | const isNodeVulnerablePath = path.resolve('./index.js') 15 | const child = childProcess.spawnSync(process.execPath, [isNodeVulnerablePath]) 16 | assert.strictEqual(child.status, 1) 17 | assert(child.stdout.toString().indexOf('is end-of-life. There are high chances of being vulnerable') !== -1) 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | id-token: write # Required for OIDC 10 | contents: read 11 | 12 | jobs: 13 | publish: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: '20' 21 | registry-url: 'https://registry.npmjs.org' 22 | 23 | # Ensure npm 11.5.1 or later is installed 24 | - name: Update npm 25 | run: npm install -g npm@latest 26 | - run: npm ci 27 | - run: npm run build --if-present 28 | - run: npm test 29 | - run: npm publish 30 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const isOldEnough = require('./eol-versions') 4 | const ascii = require('./ascii') 5 | 6 | // To guarantee support on older versions and do not drastically impact 7 | // the maintenance of this module, we check if process.version is too old 8 | // and throw EOL warning when true. 9 | if (isOldEnough(process.version)) { 10 | console.log(ascii.danger) 11 | const msg = process.version + ' is end-of-life. There are high chances of being vulnerable. Please upgrade it.' 12 | console.log(msg) 13 | process.exit(1) 14 | } else { 15 | // CLI 16 | if (require.main === module) { 17 | require('./is-vulnerable').cli(process.version, require('os').platform()) 18 | } else { 19 | module.exports = { 20 | isNodeVulnerable: require('./is-vulnerable').isNodeVulnerable 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "is-my-node-vulnerable", 3 | "version": "1.6.1", 4 | "description": "package that checks if your Node.js installation is vulnerable to known security vulnerabilities", 5 | "main": "index.js", 6 | "bin": { 7 | "is-my-node-vulnerable": "./index.js" 8 | }, 9 | "keywords": [ 10 | "security", 11 | "nodejs" 12 | ], 13 | "scripts": { 14 | "build": "ncc build action.js -o dist", 15 | "test": "npm run lint && node setup-test.js", 16 | "lint": "standard" 17 | }, 18 | "author": "RafaelGSS ", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/RafaelGSS/is-my-node-vulnerable" 22 | }, 23 | "standard": { 24 | "ignore": [ 25 | "dist/**", 26 | "ascii.js" 27 | ] 28 | }, 29 | "license": "MIT", 30 | "dependencies": { 31 | "@actions/core": "^1.10.0", 32 | "semver": "^7.3.8" 33 | }, 34 | "devDependencies": { 35 | "standard": "^17.0.0", 36 | "@vercel/ncc": "^0.36.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 RafaelGSS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | node-version: [16, 18, 20, 21, 22, 23, 24] 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Use Node.js 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | 24 | - name: Install 25 | run: | 26 | npm install 27 | 28 | - name: Run validator 29 | run: | 30 | npm run test 31 | 32 | - name: Ensure Build 33 | run: | 34 | npm run build 35 | 36 | old-versions: 37 | runs-on: ubuntu-latest 38 | strategy: 39 | matrix: 40 | node-version: [0.12, 4, 6, 8, 9, 10, 12] 41 | 42 | steps: 43 | - uses: actions/checkout@v3 44 | 45 | - name: Use Node.js 46 | uses: actions/setup-node@v3 47 | with: 48 | node-version: ${{ matrix.node-version }} 49 | 50 | - name: Run test 51 | run: | 52 | node setup-test.js 53 | -------------------------------------------------------------------------------- /ascii.js: -------------------------------------------------------------------------------- 1 | const util = require('util') 2 | 3 | const danger = '\n' + 4 | '\n' + 5 | '██████ █████ ███ ██ ██████ ███████ ███████\n' + 6 | '██ ██ ██ ██ ████ ██ ██ ██ ██ ██\n' + 7 | '██ ██ ███████ ██ ██ ██ ██ ███ █████ ███████\n' + 8 | '██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██\n' + 9 | '██████ ██ ██ ██ ████ ██████ ███████ ██ ██\n' + 10 | '\n' 11 | 12 | const allGood = '\n' + 13 | '\n' + 14 | ' █████ ██ ██ ██████ ██████ ██████ ██████ ██\n' + 15 | '██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██\n' + 16 | '███████ ██ ██ ██ ███ ██ ██ ██ ██ ██ ██ ██\n' + 17 | '██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██\n' + 18 | '██ ██ ███████ ███████ ██████ ██████ ██████ ██████ ██\n' + 19 | '\n' 20 | 21 | function escapeStyleCode (code) { 22 | return '\u001b[' + code + 'm' 23 | } 24 | 25 | function bold (text) { 26 | var left = '' 27 | var right = '' 28 | const formatCodes = util.inspect.colors.bold 29 | left += escapeStyleCode(formatCodes[0]) 30 | right = escapeStyleCode(formatCodes[1]) + right 31 | return left + text + right 32 | } 33 | 34 | const vulnerableWarning = bold('The current Node.js version (' + process.version + ') is vulnerable to the following CVEs:') 35 | 36 | var separator = '=' 37 | for (var i = 0; i < process.stdout.columns; ++i) { 38 | separator = separator + '=' 39 | } 40 | 41 | module.exports.danger = danger 42 | module.exports.allGood = allGood 43 | module.exports.bold = bold 44 | module.exports.vulnerableWarning = vulnerableWarning 45 | module.exports.separator = separator 46 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const { isNodeVulnerable } = require('./index') 3 | 4 | async function t () { 5 | // of course, this test is fragile 6 | assert.ok(await isNodeVulnerable('20.5.0')) 7 | assert.ok(await isNodeVulnerable('20.0.0')) 8 | assert.ok(await isNodeVulnerable('19.0.0')) 9 | assert.ok(await isNodeVulnerable('18.0.0')) 10 | assert.ok(await isNodeVulnerable('14.0.0')) 11 | assert.ok(await isNodeVulnerable('16.0.0')) 12 | assert.ok(await isNodeVulnerable('19.6.0')) 13 | assert.ok(await isNodeVulnerable('18.14.0')) 14 | assert.ok(await isNodeVulnerable('16.19.0')) 15 | assert.ok(await isNodeVulnerable('20.8.0')) 16 | assert.ok(await isNodeVulnerable('20.11.0')) 17 | assert.ok(await isNodeVulnerable('23.6.0')) 18 | assert.ok(await isNodeVulnerable('24.0.1')) 19 | 20 | assert.rejects(() => isNodeVulnerable('999'), /Could not fetch version information/) 21 | assert.rejects(() => isNodeVulnerable('Unobtanium'), /Could not fetch version information/) // i.e. not found 22 | 23 | // EOL 24 | assert.ok(await isNodeVulnerable('21.0.0')) 25 | assert.ok(await isNodeVulnerable('19.0.0')) 26 | assert.ok(await isNodeVulnerable('16.0.0')) 27 | assert.ok(await isNodeVulnerable('17.0.0')) 28 | assert.ok(await isNodeVulnerable('15.0.0')) 29 | assert.ok(await isNodeVulnerable('13.0.0')) 30 | assert.ok(await isNodeVulnerable('12.0.0')) 31 | assert.ok(await isNodeVulnerable('v0.12.18')) 32 | 33 | // Platform specific 34 | assert.ok(await isNodeVulnerable('22.4.0', 'win32')) 35 | assert.ok(await isNodeVulnerable('19.0.0', 'linux')) 36 | assert.ok(await isNodeVulnerable('18.0.0', 'win32')) 37 | assert.ok(await isNodeVulnerable('14.0.0', 'android')) 38 | assert.rejects(() => isNodeVulnerable('20.0.0', 'non-valid-platform'), /platform non-valid-platform is not valid. Please use aix,darwin,freebsd,linux,openbsd,sunos,win32,android/) 39 | } 40 | 41 | t() 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | .env.production 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | .parcel-cache 80 | 81 | # Next.js build output 82 | .next 83 | out 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and not Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | 109 | # Stores VSCode versions used for testing VSCode extensions 110 | .vscode-test 111 | 112 | # yarn v2 113 | .yarn/cache 114 | .yarn/unplugged 115 | .yarn/build-state.yml 116 | .yarn/install-state.gz 117 | .pnp.* 118 | 119 | # End of https://mrkandreev.name/snippets/gitignore-generator/#Node 120 | 121 | .clinic/ 122 | 123 | tags 124 | tags.* 125 | 126 | schedule.etag 127 | schedule.json 128 | security.etag 129 | security.json 130 | -------------------------------------------------------------------------------- /eol-versions.js: -------------------------------------------------------------------------------- 1 | // Sync from https://raw.githubusercontent.com/nodejs/Release/master/schedule.json 2 | // These are the list of versions that might be affected by this module dependencies 3 | const versionMap = { 4 | // v0.12 5 | v0: { 6 | start: '2015-02-06', 7 | end: '2016-12-31' 8 | }, 9 | v4: { 10 | start: '2015-09-08', 11 | lts: '2015-10-12', 12 | maintenance: '2017-04-01', 13 | end: '2018-04-30', 14 | codename: 'Argon' 15 | }, 16 | v5: { 17 | start: '2015-10-29', 18 | maintenance: '2016-04-30', 19 | end: '2016-06-30' 20 | }, 21 | v6: { 22 | start: '2016-04-26', 23 | lts: '2016-10-18', 24 | maintenance: '2018-04-30', 25 | end: '2019-04-30', 26 | codename: 'Boron' 27 | }, 28 | v7: { 29 | start: '2016-10-25', 30 | maintenance: '2017-04-30', 31 | end: '2017-06-30' 32 | }, 33 | v8: { 34 | start: '2017-05-30', 35 | lts: '2017-10-31', 36 | maintenance: '2019-01-01', 37 | end: '2019-12-31', 38 | codename: 'Carbon' 39 | }, 40 | v9: { 41 | start: '2017-10-01', 42 | maintenance: '2018-04-01', 43 | end: '2018-06-30' 44 | }, 45 | v10: { 46 | start: '2018-04-24', 47 | lts: '2018-10-30', 48 | maintenance: '2020-05-19', 49 | end: '2021-04-30', 50 | codename: 'Dubnium' 51 | }, 52 | v11: { 53 | start: '2018-10-23', 54 | maintenance: '2019-04-22', 55 | end: '2019-06-01' 56 | }, 57 | v12: { 58 | start: '2019-04-23', 59 | lts: '2019-10-21', 60 | maintenance: '2020-11-30', 61 | end: '2022-04-30', 62 | codename: 'Erbium' 63 | }, 64 | v13: { 65 | start: '2019-10-22', 66 | maintenance: '2020-04-01', 67 | end: '2020-06-01' 68 | }, 69 | v14: { 70 | start: '2020-04-21', 71 | lts: '2020-10-27', 72 | maintenance: '2021-10-19', 73 | end: '2023-04-30', 74 | codename: 'Fermium' 75 | }, 76 | v15: { 77 | start: '2020-10-20', 78 | maintenance: '2021-04-01', 79 | end: '2021-06-01' 80 | }, 81 | v16: { 82 | start: '2021-04-20', 83 | lts: '2021-10-26', 84 | maintenance: '2022-10-18', 85 | end: '2023-09-11', 86 | codename: 'Gallium' 87 | }, 88 | v17: { 89 | start: '2021-10-19', 90 | maintenance: '2022-04-01', 91 | end: '2022-06-01' 92 | }, 93 | v19: { 94 | start: '2022-10-18', 95 | maintenance: '2023-04-01', 96 | end: '2023-06-01' 97 | } 98 | } 99 | 100 | function isOldEnough (version) { 101 | const versionInfo = getVersionInfo(version) 102 | 103 | if (!versionInfo) { 104 | return false 105 | } else if (!versionInfo.end) { 106 | return true // Versions without an EOL date are considered EOL 107 | } 108 | 109 | const now = new Date() 110 | const end = new Date(versionInfo.end) 111 | return now > end 112 | } 113 | 114 | function getVersionInfo (version) { 115 | const majorVersion = extractMajorVersion(version) 116 | return versionMap[majorVersion] || null 117 | } 118 | 119 | function extractMajorVersion (version) { 120 | // Extracts the major version number from a version string like 'v12.22.12' 121 | const major = version.split('.')[0] 122 | return major 123 | } 124 | 125 | module.exports = isOldEnough 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # is-my-node-vulnerable 2 | 3 | This package helps ensure the security of your Node.js installation by checking for known vulnerabilities. 4 | It compares the version of Node.js you have installed (`process.version`) to the [Node.js Security Database][] 5 | and alerts you if a vulnerability is found. 6 | 7 | ## Usage 8 | 9 | ``` 10 | npx is-my-node-vulnerable 11 | ``` 12 | 13 | It's strongly recommended to include this as a step in the app CI. 14 | 15 | > [!NOTE] 16 | > For retro-compatibility enthusiasts: This module supports Node.js versions >= v0.12. 17 | > However, npx does not work with those older versions, so you'll need to install the 18 | > package and run index.js manually. If you encounter errors when using npx, it's 19 | > likely because you're using a vulnerable version of Node.js. Please consider upgrading. 20 | 21 | ### Output - When vulnerable 22 | 23 | 24 | ```console 25 | $ node -v 26 | v20.3.0 27 | $ npx is-my-node-vulnerable 28 | 29 | 30 | ██████ █████ ███ ██ ██████ ███████ ██████ 31 | ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ 32 | ██ ██ ███████ ██ ██ ██ ██ ███ █████ ██████ 33 | ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ 34 | ██████ ██ ██ ██ ████ ██████ ███████ ██ ██ 35 | 36 | 37 | The current Node.js version (v20.3.0) is vulnerable to the following CVEs: 38 | 39 | CVE-2023-30581: The use of proto in process.mainModule.proto.require() can bypass the policy mechanism and require modules outside of the policy.json definition 40 | Patched versions: ^16.20.1 || ^18.16.1 || ^20.3.1 41 | ================================================================================================================================================================================== 42 | ``` 43 | 44 | ### Output - When non-vulnerable 45 | 46 | ```console 47 | $ node -v 48 | v20.17.0 49 | $ npx is-my-node-vulnerable 50 | 51 | 52 | █████ ██ ██ ██████ ██████ ██████ ██████ ██ 53 | ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ 54 | ███████ ██ ██ ██ ███ ██ ██ ██ ██ ██ ██ ██ 55 | ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ 56 | ██ ██ ███████ ███████ ██████ ██████ ██████ ██████ ██ 57 | 58 | ``` 59 | 60 | ### Output - when end of life 61 | 62 | ```console 63 | $ node -v 64 | v15.14.0 65 | $ npx is-my-node-vulnerable 66 | ██████ █████ ███ ██ ██████ ███████ ██████ 67 | ██ ██ ██ ██ ████ ██ ██ ██ ██ ██ 68 | ██ ██ ███████ ██ ██ ██ ██ ███ █████ ██████ 69 | ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ 70 | ██████ ██ ██ ██ ████ ██████ ███████ ██ ██ 71 | 72 | 73 | v15.14.0 is end-of-life. There are high chances of being vulnerable. Please upgrade it. 74 | ``` 75 | 76 | End-of-Life versions don't keep track of recent security releases, therefore, it's considered vulnerable by default. 77 | 78 | ## API 79 | 80 | This package also exports a function `isNodeVulnerable` to perform the check at runtime 81 | 82 | > [!NOTE] 83 | > The API is only supported on active Node.js versions (v18.x, v20.x, v22.x, v23.x) 84 | 85 | ```js 86 | import { isNodeVulnerable } from 'is-my-node-vulnerable' 87 | 88 | await isNodeVulnerable('19.0.0') // true 89 | ``` 90 | 91 | Optionally, you can define the platform with the argument `platform` to limit the scope. The available platforms are [the same values](https://nodejs.org/api/os.html#osplatform) available in `os.platform()`. 92 | 93 | ```js 94 | import { isNodeVulnerable } from 'is-my-node-vulnerable' 95 | 96 | await isNodeVulnerable('19.0.0', 'linux') // true 97 | ``` 98 | 99 | [Node.js Security Database]: https://github.com/nodejs/security-wg/tree/main/vuln 100 | 101 | 102 | ## Github Action 103 | 104 | This package also provides a GitHub Action, just include the `node-version` in the yml as follows in order to check a specific version: 105 | 106 | ```yml 107 | name: "Node.js Vulnerabilities" 108 | on: 109 | schedule: 110 | - cron: "0 0 * * *" 111 | 112 | jobs: 113 | is-my-node-vulnerable: 114 | runs-on: ubuntu-latest 115 | steps: 116 | - uses: actions/checkout@v3 117 | - name: Check Node.js 118 | uses: nodejs/is-my-node-vulnerable@v1 119 | with: 120 | node-version: "22.15.0" 121 | ``` 122 | 123 | Optionally, you can define the platform with the argument `platform` to limit the scope. The available platforms are [the same values](https://nodejs.org/api/os.html#osplatform) available in `os.platform()`. 124 | 125 | ```yml 126 | - uses: actions/checkout@v3 127 | - name: Check Node.js 128 | uses: nodejs/is-my-node-vulnerable@v1 129 | with: 130 | node-version: "22.15.0" 131 | platform: "linux" 132 | ``` 133 | -------------------------------------------------------------------------------- /is-vulnerable.js: -------------------------------------------------------------------------------- 1 | const { danger, allGood, bold, vulnerableWarning, separator } = require('./ascii') 2 | const { request } = require('https') 3 | const fs = require('fs') 4 | const path = require('path') 5 | const satisfies = require('semver/functions/satisfies') 6 | 7 | const STORE = { 8 | security: { 9 | url: 'https://raw.githubusercontent.com/nodejs/security-wg/main/vuln/core/index.json', 10 | jsonFile: path.join(__dirname, 'security.json'), 11 | etagFile: path.join(__dirname, 'security.etag'), 12 | etagValue: '' 13 | }, 14 | schedule: { 15 | url: 'https://raw.githubusercontent.com/nodejs/Release/main/schedule.json', 16 | jsonFile: path.join(__dirname, 'schedule.json'), 17 | etagFile: path.join(__dirname, 'schedule.etag'), 18 | etagValue: '' 19 | } 20 | } 21 | 22 | async function readLocal (file) { 23 | return require(file) 24 | } 25 | 26 | function debug (msg) { 27 | if (process.env.DEBUG) { 28 | console.debug(msg) 29 | } 30 | } 31 | 32 | function loadETag () { 33 | for (const [key, obj] of Object.entries(STORE)) { 34 | if (fs.existsSync(obj.etagFile)) { 35 | debug(`Loading local ETag for '${key}'`) 36 | obj.etagValue = fs.readFileSync(obj.etagFile).toString() 37 | } 38 | } 39 | } 40 | 41 | async function fetchJson (obj) { 42 | await new Promise((resolve) => { 43 | request(obj.url, (res) => { 44 | if (res.statusCode !== 200) { 45 | console.error(`Request to Github returned http status ${res.statusCode}. Aborting...`) 46 | process.nextTick(() => { process.exit(1) }) 47 | } 48 | 49 | const fileStream = fs.createWriteStream(obj.jsonFile) 50 | res.pipe(fileStream) 51 | 52 | fileStream.on('finish', () => { 53 | fileStream.close() 54 | resolve() 55 | }) 56 | 57 | fileStream.on('error', (err) => { 58 | console.error(`Error ${err.message} while writing to '${obj.jsonFile}'. Aborting...`) 59 | process.nextTick(() => { process.exit(1) }) 60 | }) 61 | }).on('error', (err) => { 62 | console.error(`Request to Github returned error ${err.message}. Aborting...`) 63 | process.nextTick(() => { process.exit(1) }) 64 | }).end() 65 | }) 66 | return readLocal(obj.jsonFile) 67 | } 68 | 69 | async function getJson (obj) { 70 | return new Promise((resolve) => { 71 | request(obj.url, { method: 'HEAD' }, (res) => { 72 | if (res.statusCode !== 200) { 73 | console.error(`Request to Github returned http status ${res.statusCode}. Aborting...`) 74 | process.nextTick(() => { process.exit(1) }) 75 | } 76 | 77 | res.on('data', () => {}) 78 | 79 | const { etag } = res.headers 80 | if (!obj.etagValue || obj.eTagValue !== etag || !fs.existsSync(obj.jsonFile)) { 81 | obj.etagValue = etag 82 | fs.writeFileSync(obj.etagFile, etag) 83 | debug('Creating local core.json') 84 | resolve(fetchJson(obj)) 85 | } else { 86 | debug(`No updates from upstream. Getting a cached version: ${obj.jsonFile}`) 87 | resolve(readLocal(obj.jsonFile)) 88 | } 89 | }).on('error', (err) => { 90 | console.error(`Request to Github returned error ${err.message}. Aborting...`) 91 | process.nextTick(() => { process.exit(1) }) 92 | }).end() 93 | }) 94 | } 95 | 96 | const checkPlatform = platform => { 97 | const availablePlatforms = ['aix', 'darwin', 'freebsd', 'linux', 'openbsd', 'sunos', 'win32', 'android'] 98 | if (platform && !availablePlatforms.includes(platform)) { 99 | throw new Error(`platform ${platform} is not valid. Please use ${availablePlatforms.join(',')}.`) 100 | } 101 | } 102 | 103 | const isSystemAffected = (platform, affectedEnvironments) => { 104 | // No platform specified (legacy mode) 105 | if (!platform || !Array.isArray(affectedEnvironments)) { 106 | return true 107 | } 108 | // If the environment is matching or all the environments are affected 109 | if (affectedEnvironments.includes(platform) || affectedEnvironments.includes('all')) { 110 | return true 111 | } 112 | // Default to false 113 | return false 114 | } 115 | 116 | function getVulnerabilityList (currentVersion, data, platform) { 117 | const list = [] 118 | for (const key in data) { 119 | const vuln = data[key] 120 | 121 | if ( 122 | ( 123 | satisfies(currentVersion, vuln.vulnerable) && 124 | !satisfies(currentVersion, vuln.patched) 125 | ) && isSystemAffected(platform, vuln.affectedEnvironments) 126 | ) { 127 | const severity = vuln.severity === 'unknown' ? '' : `(${vuln.severity})` 128 | list.push(`${bold(vuln.cve)}${severity}: ${vuln.overview}\n${bold('Patched versions')}: ${vuln.patched}`) 129 | } 130 | } 131 | return list 132 | } 133 | 134 | async function cli (currentVersion, platform) { 135 | checkPlatform(platform) 136 | 137 | const isEOL = await isNodeEOL(currentVersion) 138 | if (isEOL) { 139 | console.error(danger) 140 | console.error(`${currentVersion} is end-of-life. There are high chances of being vulnerable. Please upgrade it.`) 141 | process.exit(1) 142 | } 143 | 144 | const securityJson = await getJson(STORE.security) 145 | const list = getVulnerabilityList(currentVersion, securityJson, platform) 146 | 147 | if (list.length) { 148 | console.error(danger) 149 | console.error(vulnerableWarning + '\n') 150 | console.error(`${list.join(`\n${separator}\n\n`)}\n${separator}`) 151 | process.exit(1) 152 | } else { 153 | console.info(allGood) 154 | } 155 | } 156 | 157 | async function getVersionInfo (version) { 158 | const scheduleJson = await getJson(STORE.schedule) 159 | 160 | if (scheduleJson[version.toLowerCase()]) { 161 | return scheduleJson[version.toLowerCase()] 162 | } 163 | 164 | for (const [key, value] of Object.entries(scheduleJson)) { 165 | if (satisfies(version, key)) { 166 | return value 167 | } 168 | } 169 | 170 | return null 171 | } 172 | 173 | /** 174 | * @param {string} version 175 | * @returns {Promise} true if the version is end-of-life 176 | */ 177 | async function isNodeEOL (version) { 178 | const myVersionInfo = await getVersionInfo(version) 179 | 180 | if (!myVersionInfo) { 181 | // i.e. isNodeEOL('abcd') or isNodeEOL('lts') or isNodeEOL('99') 182 | throw Error(`Could not fetch version information for ${version}`) 183 | } else if (!myVersionInfo.end) { 184 | // We got a record, but.. 185 | // v0.12.18 etc does not have an EOL date, which probably means too old. 186 | return true 187 | } 188 | 189 | const now = new Date() 190 | const end = new Date(myVersionInfo.end) 191 | return now > end 192 | } 193 | 194 | async function isNodeVulnerable (version, platform) { 195 | checkPlatform(platform) 196 | const isEOL = await isNodeEOL(version) 197 | if (isEOL) { 198 | return true 199 | } 200 | 201 | const coreIndex = await getJson(STORE.security) 202 | const list = getVulnerabilityList(version, coreIndex, platform) 203 | return list.length > 0 204 | } 205 | 206 | if (process.argv[2] !== '-r') { 207 | loadETag() 208 | } 209 | 210 | module.exports = { 211 | isNodeVulnerable, 212 | cli 213 | } 214 | --------------------------------------------------------------------------------