From 90dce45d320420cef8947e96e406729a32d869d0 Mon Sep 17 00:00:00 2001 From: coder9io <342252087+coder9io@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:37:41 +0000 Subject: [PATCH 1/4] Pin development Node to 24.22.0 LTS in .nvmrc Node 18 is end-of-life and will not receive the eMudhra SPKI blocklist (nodejs/node#66871). Pin the current Active LTS so contributors and CI share one version; it is bumped to the first 24.x release containing the blocklist once one is published. --- .nvmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..a43a5b9 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24.22.0 From d51d0a34871da7aeff24c21127c7a16e049cce57 Mon Sep 17 00:00:00 2001 From: coder9io <342252087+coder9io@users.noreply.github.com> Date: Wed, 21 Oct 2026 00:37:51 +0000 Subject: [PATCH 2/4] ci: read Node version from .nvmrc instead of EOL 18.19.0 CI pinned Node 18.19.0, an end-of-life line that will never get the eMudhra intermediate blocklist. Read .nvmrc so CI and local development share one pin and the follow-up bump to a patched release is a one-line change outside the workflow. Checkout moves first because setup-node needs the file; setup-node v4 replaces the deprecated v2. --- .github/workflows/build.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 80d7061..92dd27c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,9 +10,12 @@ jobs: name: Build runs-on: ubuntu-latest 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-file: .nvmrc - uses: shogo82148/actions-setup-mysql@v1.23.0 with: mysql-version: '8.0' @@ -23,9 +26,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 From 1db1c6ed9cc378d9051eed3d94ecb0e26e97a99a Mon Sep 17 00:00:00 2001 From: coder9io <342252087+coder9io@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:15:02 +0000 Subject: [PATCH 3/4] test: pin Node engines, CI matrix and .nvmrc to patched floors Node releases before 22.23.4 / 24.22.1 / 26.12.1 accept TLS chains through the compromised eMudhra intermediates (#58). The test keeps engines.node, the CI matrix and .nvmrc on the same patched floors. Adds semver (already in the lockfile) as a direct dev dependency. --- package-lock.json | 1 + package.json | 1 + tests/node-version.test.ts | 53 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) create mode 100644 tests/node-version.test.ts diff --git a/package-lock.json b/package-lock.json index 93791dd..e167585 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", diff --git a/package.json b/package.json index 245d813..7b2f5a4 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,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/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()); + }); +}); From 1048dd54a0aaf30e2160c2e8c7a1f527be0d15d0 Mon Sep 17 00:00:00 2001 From: coder9io <342252087+coder9io@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:15:26 +0000 Subject: [PATCH 4/4] Require Node releases that block the eMudhra intermediates Node 22.23.4, 24.22.1 and 26.12.1 ship the SPKI blocklist from nodejs/node#66871; earlier releases trust chains through the compromised eMudhra intermediates. OpenAPM runs inside customer apps and makes HTTPS calls to Last9, so a CI pin alone fixes nothing for them: declare the patched floor of each line in engines.node. CI now runs the suite on each floor instead of the single .nvmrc pin, plus scripts/tls-probe.mjs, which must see CERT_REVOKED from the eMudhra test endpoint and succeed against registry.npmjs.org. .nvmrc moves to 24.22.1 for local development. --- .github/workflows/build.yml | 13 +++++++++++-- .nvmrc | 2 +- CHANGELOG.md | 6 ++++++ README.md | 7 +++++++ package-lock.json | 3 +++ package.json | 3 +++ scripts/tls-probe.mjs | 34 ++++++++++++++++++++++++++++++++++ 7 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 scripts/tls-probe.mjs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 92dd27c..e1ac1fe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,15 +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: - name: ๐Ÿ›Ž Checkout uses: actions/checkout@v3 - uses: actions/setup-node@v4 with: - node-version-file: .nvmrc + 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' diff --git a/.nvmrc b/.nvmrc index a43a5b9..ce62013 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -24.22.0 +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 e167585..3ab586e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,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 7b2f5a4..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" }, 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');