diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d15d2b..fbe561f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,9 +27,15 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 20 + node-version-file: .nvmrc + check-latest: true cache: npm + # Fails on a Node.js whose bundled Brotli is vulnerable to + # CVE-2026-111497 (see FIXED in scripts/check-node-version.js). + - name: Check Node.js version + run: node scripts/check-node-version.js + - name: Install dependencies run: npm ci diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 311d1bc..b259c3f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -29,9 +29,15 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 20 + node-version-file: .nvmrc + check-latest: true cache: npm + # Fails on a Node.js whose bundled Brotli is vulnerable to + # CVE-2026-111497 (see FIXED in scripts/check-node-version.js). + - name: Check Node.js version + run: node scripts/check-node-version.js + - name: Install dependencies run: npm ci diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/package.json b/package.json index c35f718..67a2bb0 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "license": "BSD-3-Clause", "scripts": { "docusaurus": "docusaurus", - "generate": "node scripts/fetch-profile-readme.js", + "generate": "node scripts/check-node-version.js && node scripts/fetch-profile-readme.js", "prestart": "npm run generate", "start": "docusaurus start", "prebuild": "npm run generate", @@ -39,6 +39,6 @@ ] }, "engines": { - "node": ">=20.0" + "node": "^22.23.4 || ^24.22.1 || ^26.12.1" } } diff --git a/scripts/check-node-version.js b/scripts/check-node-version.js new file mode 100644 index 0000000..610b733 --- /dev/null +++ b/scripts/check-node-version.js @@ -0,0 +1,37 @@ +// Fails if the running Node.js bundles a Brotli decoder vulnerable to +// CVE-2026-111497 ("Node2Shell"): a crafted Brotli stream can corrupt +// memory in whatever decompresses it, and the build decompresses one — +// fetch() in fetch-profile-readme.js accepts `Content-Encoding: br`. +// Runs first in ci.yml and deploy.yml (before npm ci), +// and again via npm run generate. +// +// Only release lines with a fix are accepted, each from its first fixed +// version. Node 20 never got a fix, and odd lines (23, 25) are end of life. +// To move to a new line, add its first fixed version here, then update +// engines in package.json and .nvmrc. +// https://www.aikido.dev/blog/python-packages-vulnerable-node2shell-brotli-cve-2026-111497 + +const FIXED = { + 22: [22, 23, 4], + 24: [24, 22, 1], + 26: [26, 12, 1], +}; + +function isFixed(version) { + const parts = version.replace(/^v/, '').split('.').map(Number); + const minimum = FIXED[parts[0]]; + if (!minimum) return false; + for (let i = 1; i < 3; i++) { + if (parts[i] !== minimum[i]) return parts[i] > minimum[i]; + } + return true; +} + +if (require.main === module) { + if (!isFixed(process.version)) { + const accepted = Object.values(FIXED).map((v) => `>=${v.join('.')}`).join(', '); + console.error(`check-node-version.js: Node ${process.version} is vulnerable to CVE-2026-111497; use one of ${accepted}`); + process.exit(1); + } + console.log(`check-node-version.js: Node ${process.version} is not affected by CVE-2026-111497`); +}