From fe4fee6c3e09942e955150130861e546acc0ed1c Mon Sep 17 00:00:00 2001 From: Sam Skynner Date: Tue, 20 Oct 2026 21:02:40 -0500 Subject: [PATCH 1/4] fix: remove blocked eMudhra keys from Node trust --- .github/actions/build/action.yml | 2 +- .github/actions/cypress/action.yml | 2 +- instrumentation.ts | 5 +++ package.json | 11 ++--- scripts/tls-policy.cjs | 27 ++++++++++++ scripts/tls-policy.test.cjs | 69 ++++++++++++++++++++++++++++++ sentry.server.config.ts | 4 ++ 7 files changed, 113 insertions(+), 7 deletions(-) create mode 100644 instrumentation.ts create mode 100644 scripts/tls-policy.cjs create mode 100644 scripts/tls-policy.test.cjs diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index 005432b88b..f2674bf582 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -58,7 +58,7 @@ runs: - name: install shell: sh - run: pnpm install --frozen-lockfile --prefer-offline + run: NODE_OPTIONS="$NODE_OPTIONS --require \"$(pwd)/scripts/tls-policy.cjs\"" pnpm install --frozen-lockfile --prefer-offline - name: Restore next cache uses: actions/cache@v4 diff --git a/.github/actions/cypress/action.yml b/.github/actions/cypress/action.yml index 5aa3600b6e..4c1bd97158 100644 --- a/.github/actions/cypress/action.yml +++ b/.github/actions/cypress/action.yml @@ -26,7 +26,7 @@ runs: - name: install shell: sh run: | - pnpm install --frozen-lockfile --prefer-offline + NODE_OPTIONS="$NODE_OPTIONS --require \"$(pwd)/scripts/tls-policy.cjs\"" pnpm install --frozen-lockfile --prefer-offline pnpm add serve # pnpm doesn't reliably repopulate the Cypress binary cache # (~/.cache/Cypress) on warm CI runs the way yarn did, and that dir diff --git a/instrumentation.ts b/instrumentation.ts new file mode 100644 index 0000000000..10e022724e --- /dev/null +++ b/instrumentation.ts @@ -0,0 +1,5 @@ +export async function register() { + if (process.env.NEXT_RUNTIME === 'nodejs') { + require('./scripts/tls-policy.cjs'); + } +} diff --git a/package.json b/package.json index 838ac9213b..2b952c3af3 100644 --- a/package.json +++ b/package.json @@ -31,14 +31,14 @@ } }, "scripts": { - "dev": "next dev", + "dev": "NODE_OPTIONS=\"$NODE_OPTIONS --require \\\"$(pwd)/scripts/tls-policy.cjs\\\"\" next dev", "dev:special": "NODE_OPTIONS='--inspect --max_old_space_size=8000' next dev", - "build": "next build", + "build": "NODE_OPTIONS=\"$NODE_OPTIONS --require \\\"$(pwd)/scripts/tls-policy.cjs\\\"\" next build", "build:analyze": "ANALYZE=true pnpm build", - "build:static": "STATIC_EXPORT=true next build", + "build:static": "NODE_OPTIONS=\"$NODE_OPTIONS --require \\\"$(pwd)/scripts/tls-policy.cjs\\\"\" STATIC_EXPORT=true next build", "build:optimize-svgs": "find public -type f -name '*.svg' -print0 | xargs -0 -n 1 -P 6 npx svgo --final-newline", "serve:static": "npx serve out", - "start": "next start", + "start": "NODE_OPTIONS=\"$NODE_OPTIONS --require \\\"$(pwd)/scripts/tls-policy.cjs\\\"\" next start", "lint:code": "next lint --dir src --dir pages", "lint:code:fix": "next lint --dir src --dir pages --fix", "lint:formatting": "prettier src pages cypress . --check", @@ -54,7 +54,8 @@ "test:headless": "export DOTENV_CONFIG_PATH='../../../.env.local' && cypress run --config-file './cypress/configs/local/full.config.ts'", "test": "jest --watch", "test:ci": "jest --ci", - "test:coverage": "jest --coverage" + "test:coverage": "jest --coverage", + "test:tls": "node --test scripts/tls-policy.test.cjs" }, "dependencies": { "@aave-dao/aave-address-book": "^4.55.6", diff --git a/scripts/tls-policy.cjs b/scripts/tls-policy.cjs new file mode 100644 index 0000000000..1e04fa6233 --- /dev/null +++ b/scripts/tls-policy.cjs @@ -0,0 +1,27 @@ +const { createHash, X509Certificate } = require('node:crypto'); +const tls = require('node:tls'); + +// SHA-256 SPKI keys removed after the October 2026 eMudhra incident. +const blockedKeys = new Set([ + 'bd153ed7b0434f6886b17bce8bbe84ed340c7132d702a8f4fa318f756ecbd6f3', // AAA Certificate Services + '8d417db2dd8bf5e3084d1e3f196d583849d81bdd4c00c70b9d39369e96b8c782', // emSign ECC Root CA - G3 +]); + +if (typeof tls.setDefaultCACertificates !== 'function') { + throw new Error('TLS policy requires Node 22.19+ or 24.5+'); +} + +const certificates = tls.getCACertificates('default').filter((pem) => { + const publicKey = new X509Certificate(pem).publicKey.export({ + type: 'spki', + format: 'der', + }); + return !blockedKeys.has(createHash('sha256').update(publicKey).digest('hex')); +}); + +if (certificates.length === 0) { + throw new Error('TLS policy found no trusted certificates'); +} + +// Replace defaults, including configured system/extra roots, before clients start. +tls.setDefaultCACertificates(certificates); diff --git a/scripts/tls-policy.test.cjs b/scripts/tls-policy.test.cjs new file mode 100644 index 0000000000..fe8c3b3f3d --- /dev/null +++ b/scripts/tls-policy.test.cjs @@ -0,0 +1,69 @@ +const assert = require('node:assert/strict'); +const { spawnSync } = require('node:child_process'); +const path = require('node:path'); +const test = require('node:test'); + +const policy = path.join(__dirname, 'tls-policy.cjs'); +const fixtures = path.join(__dirname, 'fixtures/blocked-roots.pem'); + +function run(source, env = {}) { + const result = spawnSync(process.execPath, ['-e', source], { + encoding: 'utf8', + env: { ...process.env, NODE_OPTIONS: '', ...env }, + }); + assert.equal(result.status, 0, result.stderr || result.stdout); +} + +test('removes both blocked keys from extra roots and preserves every other default root', () => { + run( + ` + const assert = require('node:assert/strict'); + const { X509Certificate, createHash } = require('node:crypto'); + const tls = require('node:tls'); + const key = pem => createHash('sha256').update( + new X509Certificate(pem).publicKey.export({ type: 'spki', format: 'der' }) + ).digest('hex'); + const blocked = new Set([ + 'bd153ed7b0434f6886b17bce8bbe84ed340c7132d702a8f4fa318f756ecbd6f3', + '8d417db2dd8bf5e3084d1e3f196d583849d81bdd4c00c70b9d39369e96b8c782', + ]); + const before = tls.getCACertificates('default'); + assert.equal(new Set(before.map(key).filter(k => blocked.has(k))).size, 2); + const expected = new Set(before.filter(pem => !blocked.has(key(pem))).map(pem => new X509Certificate(pem).fingerprint256)); + require(${JSON.stringify(policy)}); + const after = tls.getCACertificates('default'); + assert.deepEqual(new Set(after.map(pem => new X509Certificate(pem).fingerprint256)), expected); + assert(after.some(pem => new X509Certificate(pem).subject.includes('Amazon Root CA'))); + `, + { NODE_EXTRA_CA_CERTS: fixtures } + ); +}); + +test('preload also runs in Node workers', () => { + run( + ` + const { Worker } = require('node:worker_threads'); + const assert = require('node:assert/strict'); + const worker = new Worker(\` + const { parentPort } = require('node:worker_threads'); + const { X509Certificate } = require('node:crypto'); + const tls = require('node:tls'); + parentPort.postMessage(tls.getCACertificates('default').map(pem => new X509Certificate(pem).subject)); + \`, { eval: true }); + worker.once('message', subjects => { + assert(!subjects.some(s => s.includes('AAA Certificate Services') || s.includes('emSign ECC Root CA - G3'))); + }); + worker.once('error', error => { throw error; }); + `, + { NODE_EXTRA_CA_CERTS: fixtures, NODE_OPTIONS: `--require ${JSON.stringify(policy)}` } + ); +}); + +test('fails closed when the runtime cannot replace default roots', () => { + run(` + require('node:tls').setDefaultCACertificates = undefined; + require('node:assert/strict').throws(() => require(${JSON.stringify( + policy + )}), /TLS policy requires/); + `); +}); diff --git a/sentry.server.config.ts b/sentry.server.config.ts index 8a034f0be2..9b082a1890 100644 --- a/sentry.server.config.ts +++ b/sentry.server.config.ts @@ -1,3 +1,7 @@ +// Apply the trust policy before Sentry can create a TLS client. +// eslint-disable-next-line simple-import-sort/imports +import './scripts/tls-policy.cjs'; + // This file configures the initialization of Sentry on the server. // The config you add here will be used whenever the server handles a request. // https://docs.sentry.io/platforms/javascript/guides/nextjs/ From e66a8d795304d735df91603807e7cad0fdafb43d Mon Sep 17 00:00:00 2001 From: Sam Skynner Date: Tue, 20 Oct 2026 21:03:14 -0500 Subject: [PATCH 2/4] test: include public blocked root certificate fixtures --- scripts/fixtures/blocked-roots.pem | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 scripts/fixtures/blocked-roots.pem diff --git a/scripts/fixtures/blocked-roots.pem b/scripts/fixtures/blocked-roots.pem new file mode 100644 index 0000000000..97b5413aa6 --- /dev/null +++ b/scripts/fixtures/blocked-roots.pem @@ -0,0 +1,35 @@ +-----BEGIN CERTIFICATE----- +MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEbMBkGA1UE +CAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21v +ZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTA0 +MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENB +IExpbWl0ZWQxITAfBgNVBAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686td +UIoWMQuaBtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe +3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4YgNW8Ioa +E+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZRrOme9Hg6jc8P2ULi +mAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cmez6KJcfA3Z3mNWgQIJ2P2N7S +w4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQUoBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYD +VR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDov +L2NybC5jb21vZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0 +dHA6Ly9jcmwuY29tb2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG +9w0BAQUFAAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q +GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLzRt0vxuBq +w8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2G9w84FoVxp7Z8VlI +MCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsil2D4kF501KKaU73yqWjgom7C +12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQGEwJJTjET +MBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGlt +aXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMwHhcNMTgwMjE4MTgzMDAw +WhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTEl +MCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWdu +IEVDQyBSb290IENBIC0gRzMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3i +AhKAnjlfSU2fySU0WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4P +PwT01xySfvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB +zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQD +AwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWBCUfvO6wIBHxcmbHt +RwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD+JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE----- From 7a6e63e5bbf8fb5a67d58385b3c09be5a78fab37 Mon Sep 17 00:00:00 2001 From: Sam Skynner Date: Tue, 20 Oct 2026 21:16:13 -0500 Subject: [PATCH 3/4] revert: remove custom TLS mitigation --- .github/actions/build/action.yml | 2 +- .github/actions/cypress/action.yml | 2 +- instrumentation.ts | 5 --- package.json | 11 +++-- scripts/fixtures/blocked-roots.pem | 35 --------------- scripts/tls-policy.cjs | 27 ------------ scripts/tls-policy.test.cjs | 69 ------------------------------ sentry.server.config.ts | 4 -- 8 files changed, 7 insertions(+), 148 deletions(-) delete mode 100644 instrumentation.ts delete mode 100644 scripts/fixtures/blocked-roots.pem delete mode 100644 scripts/tls-policy.cjs delete mode 100644 scripts/tls-policy.test.cjs diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index f2674bf582..005432b88b 100644 --- a/.github/actions/build/action.yml +++ b/.github/actions/build/action.yml @@ -58,7 +58,7 @@ runs: - name: install shell: sh - run: NODE_OPTIONS="$NODE_OPTIONS --require \"$(pwd)/scripts/tls-policy.cjs\"" pnpm install --frozen-lockfile --prefer-offline + run: pnpm install --frozen-lockfile --prefer-offline - name: Restore next cache uses: actions/cache@v4 diff --git a/.github/actions/cypress/action.yml b/.github/actions/cypress/action.yml index 4c1bd97158..5aa3600b6e 100644 --- a/.github/actions/cypress/action.yml +++ b/.github/actions/cypress/action.yml @@ -26,7 +26,7 @@ runs: - name: install shell: sh run: | - NODE_OPTIONS="$NODE_OPTIONS --require \"$(pwd)/scripts/tls-policy.cjs\"" pnpm install --frozen-lockfile --prefer-offline + pnpm install --frozen-lockfile --prefer-offline pnpm add serve # pnpm doesn't reliably repopulate the Cypress binary cache # (~/.cache/Cypress) on warm CI runs the way yarn did, and that dir diff --git a/instrumentation.ts b/instrumentation.ts deleted file mode 100644 index 10e022724e..0000000000 --- a/instrumentation.ts +++ /dev/null @@ -1,5 +0,0 @@ -export async function register() { - if (process.env.NEXT_RUNTIME === 'nodejs') { - require('./scripts/tls-policy.cjs'); - } -} diff --git a/package.json b/package.json index 2b952c3af3..838ac9213b 100644 --- a/package.json +++ b/package.json @@ -31,14 +31,14 @@ } }, "scripts": { - "dev": "NODE_OPTIONS=\"$NODE_OPTIONS --require \\\"$(pwd)/scripts/tls-policy.cjs\\\"\" next dev", + "dev": "next dev", "dev:special": "NODE_OPTIONS='--inspect --max_old_space_size=8000' next dev", - "build": "NODE_OPTIONS=\"$NODE_OPTIONS --require \\\"$(pwd)/scripts/tls-policy.cjs\\\"\" next build", + "build": "next build", "build:analyze": "ANALYZE=true pnpm build", - "build:static": "NODE_OPTIONS=\"$NODE_OPTIONS --require \\\"$(pwd)/scripts/tls-policy.cjs\\\"\" STATIC_EXPORT=true next build", + "build:static": "STATIC_EXPORT=true next build", "build:optimize-svgs": "find public -type f -name '*.svg' -print0 | xargs -0 -n 1 -P 6 npx svgo --final-newline", "serve:static": "npx serve out", - "start": "NODE_OPTIONS=\"$NODE_OPTIONS --require \\\"$(pwd)/scripts/tls-policy.cjs\\\"\" next start", + "start": "next start", "lint:code": "next lint --dir src --dir pages", "lint:code:fix": "next lint --dir src --dir pages --fix", "lint:formatting": "prettier src pages cypress . --check", @@ -54,8 +54,7 @@ "test:headless": "export DOTENV_CONFIG_PATH='../../../.env.local' && cypress run --config-file './cypress/configs/local/full.config.ts'", "test": "jest --watch", "test:ci": "jest --ci", - "test:coverage": "jest --coverage", - "test:tls": "node --test scripts/tls-policy.test.cjs" + "test:coverage": "jest --coverage" }, "dependencies": { "@aave-dao/aave-address-book": "^4.55.6", diff --git a/scripts/fixtures/blocked-roots.pem b/scripts/fixtures/blocked-roots.pem deleted file mode 100644 index 97b5413aa6..0000000000 --- a/scripts/fixtures/blocked-roots.pem +++ /dev/null @@ -1,35 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEbMBkGA1UE -CAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21v -ZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTA0 -MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdy -ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENB -IExpbWl0ZWQxITAfBgNVBAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZI -hvcNAQEBBQADggEPADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686td -UIoWMQuaBtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe -3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4YgNW8Ioa -E+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZRrOme9Hg6jc8P2ULi -mAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cmez6KJcfA3Z3mNWgQIJ2P2N7S -w4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQUoBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYD -VR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDov -L2NybC5jb21vZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0 -dHA6Ly9jcmwuY29tb2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG -9w0BAQUFAAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q -GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLzRt0vxuBq -w8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2G9w84FoVxp7Z8VlI -MCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsil2D4kF501KKaU73yqWjgom7C -12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQGEwJJTjET -MBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGlt -aXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMwHhcNMTgwMjE4MTgzMDAw -WhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTEl -MCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWdu -IEVDQyBSb290IENBIC0gRzMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3i -AhKAnjlfSU2fySU0WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4P -PwT01xySfvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB -zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQD -AwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWBCUfvO6wIBHxcmbHt -RwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD+JbNR6iC8hZVdyR+EhCVBCyj ------END CERTIFICATE----- diff --git a/scripts/tls-policy.cjs b/scripts/tls-policy.cjs deleted file mode 100644 index 1e04fa6233..0000000000 --- a/scripts/tls-policy.cjs +++ /dev/null @@ -1,27 +0,0 @@ -const { createHash, X509Certificate } = require('node:crypto'); -const tls = require('node:tls'); - -// SHA-256 SPKI keys removed after the October 2026 eMudhra incident. -const blockedKeys = new Set([ - 'bd153ed7b0434f6886b17bce8bbe84ed340c7132d702a8f4fa318f756ecbd6f3', // AAA Certificate Services - '8d417db2dd8bf5e3084d1e3f196d583849d81bdd4c00c70b9d39369e96b8c782', // emSign ECC Root CA - G3 -]); - -if (typeof tls.setDefaultCACertificates !== 'function') { - throw new Error('TLS policy requires Node 22.19+ or 24.5+'); -} - -const certificates = tls.getCACertificates('default').filter((pem) => { - const publicKey = new X509Certificate(pem).publicKey.export({ - type: 'spki', - format: 'der', - }); - return !blockedKeys.has(createHash('sha256').update(publicKey).digest('hex')); -}); - -if (certificates.length === 0) { - throw new Error('TLS policy found no trusted certificates'); -} - -// Replace defaults, including configured system/extra roots, before clients start. -tls.setDefaultCACertificates(certificates); diff --git a/scripts/tls-policy.test.cjs b/scripts/tls-policy.test.cjs deleted file mode 100644 index fe8c3b3f3d..0000000000 --- a/scripts/tls-policy.test.cjs +++ /dev/null @@ -1,69 +0,0 @@ -const assert = require('node:assert/strict'); -const { spawnSync } = require('node:child_process'); -const path = require('node:path'); -const test = require('node:test'); - -const policy = path.join(__dirname, 'tls-policy.cjs'); -const fixtures = path.join(__dirname, 'fixtures/blocked-roots.pem'); - -function run(source, env = {}) { - const result = spawnSync(process.execPath, ['-e', source], { - encoding: 'utf8', - env: { ...process.env, NODE_OPTIONS: '', ...env }, - }); - assert.equal(result.status, 0, result.stderr || result.stdout); -} - -test('removes both blocked keys from extra roots and preserves every other default root', () => { - run( - ` - const assert = require('node:assert/strict'); - const { X509Certificate, createHash } = require('node:crypto'); - const tls = require('node:tls'); - const key = pem => createHash('sha256').update( - new X509Certificate(pem).publicKey.export({ type: 'spki', format: 'der' }) - ).digest('hex'); - const blocked = new Set([ - 'bd153ed7b0434f6886b17bce8bbe84ed340c7132d702a8f4fa318f756ecbd6f3', - '8d417db2dd8bf5e3084d1e3f196d583849d81bdd4c00c70b9d39369e96b8c782', - ]); - const before = tls.getCACertificates('default'); - assert.equal(new Set(before.map(key).filter(k => blocked.has(k))).size, 2); - const expected = new Set(before.filter(pem => !blocked.has(key(pem))).map(pem => new X509Certificate(pem).fingerprint256)); - require(${JSON.stringify(policy)}); - const after = tls.getCACertificates('default'); - assert.deepEqual(new Set(after.map(pem => new X509Certificate(pem).fingerprint256)), expected); - assert(after.some(pem => new X509Certificate(pem).subject.includes('Amazon Root CA'))); - `, - { NODE_EXTRA_CA_CERTS: fixtures } - ); -}); - -test('preload also runs in Node workers', () => { - run( - ` - const { Worker } = require('node:worker_threads'); - const assert = require('node:assert/strict'); - const worker = new Worker(\` - const { parentPort } = require('node:worker_threads'); - const { X509Certificate } = require('node:crypto'); - const tls = require('node:tls'); - parentPort.postMessage(tls.getCACertificates('default').map(pem => new X509Certificate(pem).subject)); - \`, { eval: true }); - worker.once('message', subjects => { - assert(!subjects.some(s => s.includes('AAA Certificate Services') || s.includes('emSign ECC Root CA - G3'))); - }); - worker.once('error', error => { throw error; }); - `, - { NODE_EXTRA_CA_CERTS: fixtures, NODE_OPTIONS: `--require ${JSON.stringify(policy)}` } - ); -}); - -test('fails closed when the runtime cannot replace default roots', () => { - run(` - require('node:tls').setDefaultCACertificates = undefined; - require('node:assert/strict').throws(() => require(${JSON.stringify( - policy - )}), /TLS policy requires/); - `); -}); diff --git a/sentry.server.config.ts b/sentry.server.config.ts index 9b082a1890..8a034f0be2 100644 --- a/sentry.server.config.ts +++ b/sentry.server.config.ts @@ -1,7 +1,3 @@ -// Apply the trust policy before Sentry can create a TLS client. -// eslint-disable-next-line simple-import-sort/imports -import './scripts/tls-policy.cjs'; - // This file configures the initialization of Sentry on the server. // The config you add here will be used whenever the server handles a request. // https://docs.sentry.io/platforms/javascript/guides/nextjs/ From c53f4fa53ea6d3768c04ccc3cd738dbaeacde39c Mon Sep 17 00:00:00 2001 From: Sam Mason de Caires Date: Wed, 21 Oct 2026 15:16:30 +0100 Subject: [PATCH 4/4] test: check node version --- next.config.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/next.config.js b/next.config.js index 5ed951774f..6b384cf641 100644 --- a/next.config.js +++ b/next.config.js @@ -20,6 +20,8 @@ const enableDevPages = process.env.NODE_ENV === 'development'); if (enableDevPages) pageExtensions.push('dev.tsx'); +console.log('Node.js version:', process.version); + /** @type {import('next').NextConfig} */ module.exports = withSentryConfig( withBundleAnalyzer({