diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 80d7061..e1ac1fe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,12 +7,24 @@ on: jobs: release: - name: Build + name: Build (Node ${{ matrix.node-version }}) runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + # The patched floor of each line in package.json engines.node (#58); + # tests/node-version.test.ts keeps them equal. + node-version: ['22.23.4', '24.22.1', '26.12.1'] steps: - - uses: actions/setup-node@v2 + - name: ๐Ÿ›Ž Checkout + uses: actions/checkout@v3 + + - uses: actions/setup-node@v4 with: - node-version: 18.19.0 + node-version: ${{ matrix.node-version }} + + - name: ๐Ÿ” TLS probe (eMudhra chain must be rejected) + run: node scripts/tls-probe.mjs - uses: shogo82148/actions-setup-mysql@v1.23.0 with: mysql-version: '8.0' @@ -23,9 +35,6 @@ jobs: - name: Create test DB run: mysql -h localhost -u root -e 'CREATE DATABASE test_db' - - name: ๐Ÿ›Ž Checkout - uses: actions/checkout@v3 - - name: ๐Ÿงถ Get NPM cache uses: actions/cache@v3 id: cache-npm diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..ce62013 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24.22.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index a0e0f1c..da1c27f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Security + +- Require Node.js `^22.23.4 || ^24.22.1 || >=26.12.1` via `engines.node`. Earlier releases trust TLS chains through the compromised eMudhra intermediates ([nodejs/node#66871](https://github.com/nodejs/node/pull/66871)). + ## [0.9.2] ### Added diff --git a/README.md b/README.md index 3ffe3a4..1154fc7 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,11 @@ An APM solution based on metrics and open-source tools such as Prometheus and Gr npm install --save @last9/openapm@latest ``` +Requires Node.js `^22.23.4 || ^24.22.1 || >=26.12.1`. Earlier releases trust +TLS chains through the compromised eMudhra intermediates +([nodejs/node#66871](https://github.com/nodejs/node/pull/66871)), which affects +the HTTPS calls OpenAPM makes to Last9. + ## Usage ```js @@ -277,6 +282,8 @@ to get familiar with the DSL for defining custom metrics. Make sure you are in the express directory. +- Use the Node.js version in `.nvmrc` (`nvm use`) + - Install packages ``` diff --git a/package-lock.json b/package-lock.json index 93791dd..3ab586e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,6 +52,7 @@ "prettier": "2.8.8", "prisma": "^5.13.0", "react": "^18.2.0", + "semver": "^6.3.1", "supertest": "^6.3.3", "tiny-glob": "^0.2.9", "ts-node": "^10.9.1", @@ -60,6 +61,9 @@ "typescript": "^5.1.6", "vitest": "^0.32.4" }, + "engines": { + "node": "^22.23.4 || ^24.22.1 || >=26.12.1" + }, "peerDependencies": { "express": "4.x" }, diff --git a/package.json b/package.json index 245d813..88e4135 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,9 @@ "Tushar Choudhari (https://github.com/chtushar)", "Aniket Rao (https://github.com/anik3tra0)" ], + "engines": { + "node": "^22.23.4 || ^24.22.1 || >=26.12.1" + }, "peerDependencies": { "express": "4.x" }, @@ -58,6 +61,7 @@ "prettier": "2.8.8", "prisma": "^5.13.0", "react": "^18.2.0", + "semver": "^6.3.1", "supertest": "^6.3.3", "tiny-glob": "^0.2.9", "ts-node": "^10.9.1", diff --git a/scripts/tls-probe.mjs b/scripts/tls-probe.mjs new file mode 100644 index 0000000..c65ae8e --- /dev/null +++ b/scripts/tls-probe.mjs @@ -0,0 +1,34 @@ +// Acceptance probe for #58: this Node runtime must reject a chain through a +// compromised eMudhra intermediate with CERT_REVOKED (nodejs/node#66871) +// while still trusting a normal HTTPS endpoint. Exits non-zero otherwise. +import https from 'node:https'; + +const BLOCKED = 'https://dv-g3b-emudhra.we-have-a-problem.com/'; +const TRUSTED = 'https://registry.npmjs.org/'; + +const connect = (url) => + new Promise((resolve) => { + const req = https.get(url, { timeout: 15000 }, (res) => { + res.resume(); + resolve({ ok: true, status: res.statusCode }); + }); + req.on('timeout', () => req.destroy(new Error('timeout'))); + req.on('error', (err) => + resolve({ ok: false, code: err.code ?? err.message }) + ); + }); + +const blocked = await connect(BLOCKED); +const trusted = await connect(TRUSTED); + +console.log(`process.version ${process.version}`); +console.log(`${BLOCKED} ${JSON.stringify(blocked)}`); +console.log(`${TRUSTED} ${JSON.stringify(trusted)}`); + +if (blocked.code !== 'CERT_REVOKED' || !trusted.ok) { + console.error( + 'TLS probe failed: expected CERT_REVOKED for the eMudhra chain and success for the trusted endpoint' + ); + process.exit(1); +} +console.log('TLS probe passed'); diff --git a/tests/node-version.test.ts b/tests/node-version.test.ts new file mode 100644 index 0000000..d6a3032 --- /dev/null +++ b/tests/node-version.test.ts @@ -0,0 +1,53 @@ +import { readFileSync } from 'fs'; +import { join } from 'path'; +import semver from 'semver'; +import { describe, expect, test } from 'vitest'; + +// Node releases before these patches accept TLS chains through the +// compromised eMudhra intermediates (nodejs/node#66871, issue #58). +const PATCHED_FLOORS = ['22.23.4', '24.22.1', '26.12.1']; +const UNPATCHED = [ + '18.19.0', + '20.19.5', + '22.23.3', + '23.11.1', + '24.22.0', + '25.9.0', + '26.12.0' +]; + +const root = join(__dirname, '..'); +const read = (file: string) => readFileSync(join(root, file), 'utf8'); +const engines: string = JSON.parse(read('package.json')).engines?.node ?? ''; + +const ciNodeVersions = () => { + const matrix = read('.github/workflows/build.yml').match( + /node-version:\s*\[([^\]]*)\]/ + ); + return matrix + ? matrix[1].split(',').map((v) => v.trim().replace(/['"]/g, '')) + : []; +}; + +describe('Node runtime pins', () => { + test('engines.node admits each patched floor', () => { + for (const version of PATCHED_FLOORS) { + expect(semver.satisfies(version, engines), version).toBe(true); + } + }); + + test('engines.node rejects releases without the eMudhra blocklist', () => { + expect(engines).not.toBe(''); + for (const version of UNPATCHED) { + expect(semver.satisfies(version, engines), version).toBe(false); + } + }); + + test('CI tests exactly the patched floors', () => { + expect(ciNodeVersions()).toEqual(PATCHED_FLOORS); + }); + + test('.nvmrc pins a CI-tested patched floor', () => { + expect(PATCHED_FLOORS).toContain(read('.nvmrc').trim()); + }); +});