mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
crypto: reset random caches across snapshots
Startup snapshots retain unused randomInt() and UUID cache entries, causing deserialized processes to replay the same random values. Clear the caches after user serialization callbacks and before user deserialization callbacks. The latter also handles caches refilled by microtasks, next ticks, or exit handlers after serialization callbacks. Refs: https://hackerone.com/reports/4059814 Signed-off-by: Filip Skokan <panva.ip@gmail.com> Assisted-by: Codex PR-URL: https://github.com/nodejs-private/node-private/pull/987 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Ruy Adorno <ruy@vlt.sh> CVE-ID: CVE-2026-106074
This commit is contained in:
1 parent
6b88896652
commit
8c44e38dc6
4 files changed
+172
-1
No files matched your search
@@ -21,6 +21,7 @@ const {
|
||||
NumberPrototypeToString,
|
||||
StringFromCharCodeApply,
|
||||
StringPrototypePadStart,
|
||||
TypedArrayPrototypeFill,
|
||||
TypedArrayPrototypeGetBuffer,
|
||||
TypedArrayPrototypeGetByteLength,
|
||||
TypedArrayPrototypeGetLength,
|
||||
@@ -75,6 +76,14 @@ const {
|
||||
|
||||
const { FastBuffer } = require('internal/buffer');
|
||||
|
||||
const {
|
||||
namespace: {
|
||||
isBuildingSnapshot,
|
||||
},
|
||||
addAfterUserSerializeCallback,
|
||||
addBeforeUserDeserializeCallback,
|
||||
} = require('internal/v8/startup_snapshot');
|
||||
|
||||
const kMaxInt32 = 2 ** 31 - 1;
|
||||
const kMaxPossibleLength = MathMin(kMaxLength, kMaxInt32);
|
||||
|
||||
@@ -388,6 +397,27 @@ let uuidData;
|
||||
let uuidNotBuffered;
|
||||
let uuidBatch = 0;
|
||||
|
||||
if (isBuildingSnapshot()) {
|
||||
const resetRandomCaches = () => {
|
||||
TypedArrayPrototypeFill(randomCache, 0);
|
||||
randomCacheOffset = randomCache.length;
|
||||
if (uuidData !== undefined) {
|
||||
TypedArrayPrototypeFill(uuidData, 0);
|
||||
uuidData = undefined;
|
||||
}
|
||||
if (uuidNotBuffered !== undefined) {
|
||||
TypedArrayPrototypeFill(uuidNotBuffered, 0);
|
||||
uuidNotBuffered = undefined;
|
||||
}
|
||||
uuidBatch = 0;
|
||||
};
|
||||
// Clear cached random data after user serialization callbacks.
|
||||
addAfterUserSerializeCallback(resetRandomCaches);
|
||||
// Microtasks and exit handlers can refill the caches after serialization
|
||||
// callbacks, so also reset them before any user deserialization callbacks.
|
||||
addBeforeUserDeserializeCallback(resetRandomCaches);
|
||||
}
|
||||
|
||||
let hexBytesCache;
|
||||
function getHexBytes() {
|
||||
if (hexBytesCache === undefined) {
|
||||
|
||||
@@ -35,8 +35,14 @@ function throwIfBuildingSnapshot(reason) {
|
||||
}
|
||||
|
||||
const deserializeCallbacks = [];
|
||||
const beforeUserDeserializeCallbacks = [];
|
||||
let deserializeCallbackIsSet = false;
|
||||
function runDeserializeCallbacks() {
|
||||
for (let i = 0; i < beforeUserDeserializeCallbacks.length; i++) {
|
||||
const { 0: callback, 1: data } = beforeUserDeserializeCallbacks[i];
|
||||
callback(data);
|
||||
}
|
||||
beforeUserDeserializeCallbacks.length = 0;
|
||||
for (let i = 0; i < deserializeCallbacks.length; i++) {
|
||||
const { 0: callback, 1: data } = deserializeCallbacks[i];
|
||||
callback(data);
|
||||
@@ -45,6 +51,14 @@ function runDeserializeCallbacks() {
|
||||
}
|
||||
|
||||
function addDeserializeCallback(callback, data) {
|
||||
addDeserializeCallbackImpl(deserializeCallbacks, callback, data);
|
||||
}
|
||||
|
||||
function addBeforeUserDeserializeCallback(callback, data) {
|
||||
addDeserializeCallbackImpl(beforeUserDeserializeCallbacks, callback, data);
|
||||
}
|
||||
|
||||
function addDeserializeCallbackImpl(callbacks, callback, data) {
|
||||
throwIfNotBuildingSnapshot();
|
||||
validateFunction(callback, 'callback');
|
||||
if (!deserializeCallbackIsSet) {
|
||||
@@ -55,7 +69,7 @@ function addDeserializeCallback(callback, data) {
|
||||
setDeserializeCallback(runDeserializeCallbacks);
|
||||
deserializeCallbackIsSet = true;
|
||||
}
|
||||
deserializeCallbacks.push([callback, data]);
|
||||
callbacks.push([callback, data]);
|
||||
}
|
||||
|
||||
const serializeCallbacks = [];
|
||||
@@ -130,4 +144,5 @@ module.exports = {
|
||||
isBuildingSnapshot,
|
||||
},
|
||||
addAfterUserSerializeCallback,
|
||||
addBeforeUserDeserializeCallback,
|
||||
};
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert');
|
||||
const {
|
||||
addDeserializeCallback,
|
||||
addSerializeCallback,
|
||||
setDeserializeMainFunction,
|
||||
} = require('node:v8').startupSnapshot;
|
||||
|
||||
// Register before loading crypto to ensure its cleanup takes precedence even
|
||||
// over user callbacks that were registered earlier.
|
||||
let deserialized;
|
||||
addDeserializeCallback(() => {
|
||||
deserialized = sample();
|
||||
});
|
||||
|
||||
const {
|
||||
randomInt,
|
||||
randomUUID,
|
||||
randomUUIDv7,
|
||||
} = require('node:crypto');
|
||||
|
||||
const buildValues = [];
|
||||
const max = 2 ** 48 - 1;
|
||||
|
||||
function primeCaches() {
|
||||
buildValues.push(
|
||||
randomUUID(),
|
||||
randomInt(max),
|
||||
randomUUID({ disableEntropyCache: true }),
|
||||
);
|
||||
}
|
||||
|
||||
switch (process.env.NODE_TEST_CRYPTO_RANDOM_MODE) {
|
||||
case 'uuid':
|
||||
primeCaches();
|
||||
break;
|
||||
case 'uuidv7':
|
||||
buildValues.push(
|
||||
randomUUIDv7(),
|
||||
randomInt(max),
|
||||
randomUUIDv7({ disableEntropyCache: true }),
|
||||
);
|
||||
break;
|
||||
case 'object-url':
|
||||
URL.revokeObjectURL(URL.createObjectURL(new Blob(['snapshot'])));
|
||||
break;
|
||||
case 'async':
|
||||
randomInt(max, (err, value) => {
|
||||
assert.ifError(err);
|
||||
buildValues.push(value);
|
||||
});
|
||||
break;
|
||||
case 'serialize':
|
||||
primeCaches();
|
||||
// A user callback can refill the caches after an earlier cleanup callback.
|
||||
addSerializeCallback(primeCaches);
|
||||
break;
|
||||
case 'microtask':
|
||||
primeCaches();
|
||||
addSerializeCallback(() => queueMicrotask(primeCaches));
|
||||
break;
|
||||
case 'next-tick':
|
||||
primeCaches();
|
||||
addSerializeCallback(() => process.nextTick(primeCaches));
|
||||
break;
|
||||
case 'exit':
|
||||
primeCaches();
|
||||
process.once('exit', primeCaches);
|
||||
break;
|
||||
case 'unprimed':
|
||||
break;
|
||||
default:
|
||||
assert.fail('Unknown cache priming mode');
|
||||
}
|
||||
|
||||
function sample() {
|
||||
return {
|
||||
uuid: Array.from({ length: 4 }, () => randomUUID()),
|
||||
// Exclude the timestamp and version so clock changes cannot hide replay.
|
||||
uuidv7: Array.from({ length: 4 }, () => randomUUIDv7().slice(15)),
|
||||
webcrypto: Array.from({ length: 4 }, () => globalThis.crypto.randomUUID()),
|
||||
int: Array.from({ length: 4 }, () => randomInt(max)),
|
||||
};
|
||||
}
|
||||
|
||||
setDeserializeMainFunction(async () => {
|
||||
const main = sample();
|
||||
main.asyncInt = await Promise.all(Array.from({ length: 4 }, () => new Promise((resolve, reject) => {
|
||||
randomInt(max, (err, value) => {
|
||||
if (err) reject(err);
|
||||
else resolve(value);
|
||||
});
|
||||
})));
|
||||
console.log(JSON.stringify({ buildValues, deserialized, main }));
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
const common = require('../common');
|
||||
if (!common.hasCrypto)
|
||||
common.skip('missing crypto');
|
||||
|
||||
const assert = require('node:assert');
|
||||
const fixtures = require('../common/fixtures');
|
||||
const tmpdir = require('../common/tmpdir');
|
||||
const { buildSnapshot, runWithSnapshot } = require('../common/snapshot');
|
||||
|
||||
const entry = fixtures.path('snapshot', 'crypto-random-cache.js');
|
||||
|
||||
tmpdir.refresh();
|
||||
for (const mode of [
|
||||
'uuid', 'uuidv7', 'object-url', 'async', 'serialize',
|
||||
'microtask', 'next-tick', 'exit', 'unprimed',
|
||||
]) {
|
||||
buildSnapshot(entry, { NODE_TEST_CRYPTO_RANDOM_MODE: mode });
|
||||
const a = JSON.parse(runWithSnapshot().stdout);
|
||||
const b = JSON.parse(runWithSnapshot().stdout);
|
||||
|
||||
// Values retained by the application at build time are intentionally shared.
|
||||
assert.deepStrictEqual(a.buildValues, b.buildValues);
|
||||
for (const phase of ['deserialized', 'main']) {
|
||||
for (const api of Object.keys(a[phase])) {
|
||||
assert.notDeepStrictEqual(a[phase][api], b[phase][api], `${mode}: ${phase} ${api}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user