Fix: safe serialise TypedArrays to avoid loosing constructor information (#5941)

* fix: enhance cleanJson to support serialization of typed arrays

* fix: correctness of inference based checks

* fix: remove duplicate Uint8Array reference

* fix: correct export syntax for mixinTypedArrays

Updated the export statement to use 'exports' instead of 'export' for proper module export functionality.

* chore: code cleanup

* test: add basics tests for cleanJson
This commit is contained in:
Siddharth Gelera (reaper)
2025-10-31 17:23:48 +05:30
committed by GitHub
parent 08c182a875
commit e47d1ed353
6 changed files with 142 additions and 2 deletions

View File

@@ -35,6 +35,7 @@ const cheerio = require('cheerio');
const tv4 = require('tv4');
const jsonwebtoken = require('jsonwebtoken');
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
const { mixinTypedArrays } = require('../sandbox/mixins/typed-arrays');
class ScriptRuntime {
constructor(props) {
@@ -94,6 +95,10 @@ class ScriptRuntime {
__brunoTestResults: __brunoTestResults
};
if (this.runtime === 'vm2') {
mixinTypedArrays(context);
}
if (onConsoleLog && typeof onConsoleLog === 'function') {
const customLogger = (type) => {
return (...args) => {
@@ -265,6 +270,10 @@ class ScriptRuntime {
__brunoTestResults: __brunoTestResults
};
if (this.runtime === 'vm2') {
mixinTypedArrays(context);
}
if (onConsoleLog && typeof onConsoleLog === 'function') {
const customLogger = (type) => {
return (...args) => {

View File

@@ -37,6 +37,7 @@ const cheerio = require('cheerio');
const tv4 = require('tv4');
const jsonwebtoken = require('jsonwebtoken');
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
const { mixinTypedArrays } = require('../sandbox/mixins/typed-arrays');
class TestRuntime {
constructor(props) {
@@ -108,6 +109,10 @@ class TestRuntime {
jwt: jsonwebtoken
};
if (this.runtime === 'vm2') {
mixinTypedArrays(context);
}
if (onConsoleLog && typeof onConsoleLog === 'function') {
const customLogger = (type) => {
return (...args) => {

View File

@@ -0,0 +1,15 @@
exports.mixinTypedArrays = (obj) => {
Object.assign(obj, {
Int8Array: Int8Array,
Uint8Array: Uint8Array,
Uint8ClampedArray: Uint8ClampedArray,
Int16Array: Int16Array,
Uint16Array: Uint16Array,
Int32Array: Int32Array,
Uint32Array: Uint32Array,
Float32Array: Float32Array,
Float64Array: Float64Array,
BigInt64Array: BigInt64Array,
BigUint64Array: BigUint64Array
});
};

View File

@@ -4,6 +4,7 @@ const path = require('node:path');
const { get } = require('lodash');
const lodash = require('lodash');
const { cleanJson } = require('../../utils');
const { mixinTypedArrays } = require('../mixins/typed-arrays');
class ScriptError extends Error {
constructor(error, script) {
@@ -60,6 +61,8 @@ async function runScriptInNodeVm({
clearImmediate: global.clearImmediate
};
mixinTypedArrays(scriptContext);
// Create shared cache for local modules
const localModuleCache = new Map();

View File

@@ -136,10 +136,58 @@ const createResponseParser = (response = {}) => {
* Remove the cleanJson fix and execute the below post response script
* bru.setVar("a", {b:3});
* Todo: Find a better fix
*
* serializes typedArrays by using Buffer to handle most binary cases
* // TODO: reaper, replace with `devalue` after evaluating all cases, current setup is
* more of a hotfix
*/
const cleanJson = (data) => {
const typedArrays = [
// Baseline typed arrays
Int8Array,
Uint8Array,
Uint8ClampedArray,
Int16Array,
Uint16Array,
Int32Array,
Uint32Array,
Float32Array,
Float64Array,
BigInt64Array,
BigUint64Array,
// Baseline 2025 Newly available
'Float16Array' in globalThis ? Float16Array : null
].filter(Boolean);
const binaryNames = typedArrays.map((d) => d.name);
const replacer = (key, value) => {
const isBinary = typedArrays.find((d) => value instanceof d);
if (isBinary) {
return {
__cleanJSONType: isBinary.name,
__cleanJSONValue: Buffer.from(value.buffer).toJSON()
};
}
return value;
};
const reviver = (key, value) => {
if (typeof value !== 'object' || value === null) {
return value;
}
if ('__cleanJSONType' in value && '__cleanJSONValue' in value) {
const matchedName = binaryNames.find((d) => value.__cleanJSONType === d);
if (!matchedName) return value;
const binConstructor = typedArrays.find((d) => d.name === matchedName);
return binConstructor.from(Buffer.from(value.__cleanJSONValue));
}
return value;
};
try {
return JSON.parse(JSON.stringify(data));
return JSON.parse(JSON.stringify(data, replacer), reviver);
} catch (e) {
return data;
}

View File

@@ -1,5 +1,5 @@
const { describe, it, expect } = require('@jest/globals');
const { evaluateJsExpression, internalExpressionCache: cache, createResponseParser } = require('../src/utils');
const { evaluateJsExpression, internalExpressionCache: cache, createResponseParser, cleanJson } = require('../src/utils');
describe('utils', () => {
describe('expression evaluation', () => {
@@ -153,4 +153,64 @@ describe('utils', () => {
expect(value).toBe(20);
});
});
describe('cleanJson', () => {
it('primitives should be kept as is', () => {
const input = {
number: 1,
string: 'hello world',
booleanFalse: false,
booleanTrue: true,
float: 2.1,
floatDeep: 2.2222222
};
expect(cleanJson(input)).toEqual(input);
});
it('functions are lost', () => {
const func = function (x, y) {
return x + y;
};
const input = {
func,
number: 1
};
expect(cleanJson(input)).toEqual({
number: 1
});
});
it('dates are serialized', () => {
const date = new Date();
const str = date.toISOString();
const input = {
date
};
expect(cleanJson(input)).toEqual({
date: str
});
});
it('typed arrays should be kept as is', () => {
const input = {
Int8Array: Int8Array.from(Buffer.from('hello world').toString()),
Uint8Array: Uint8Array.from(Buffer.from('hello world').toString()),
Uint8ClampedArray: Uint8ClampedArray.from(Buffer.from('hello world').toString()),
Int16Array: Int16Array.from(Buffer.from('hello world').toString()),
Uint16Array: Uint16Array.from(Buffer.from('hello world').toString()),
Int32Array: Int32Array.from(Buffer.from('hello world').toString()),
Uint32Array: Uint32Array.from(Buffer.from('hello world').toString()),
Float32Array: Float32Array.from(Buffer.from('hello world').toString()),
Float64Array: Float64Array.from(Buffer.from('hello world').toString()),
BigInt64Array: BigInt64Array.from(Buffer.from('123').toString()),
BigUint64Array: BigUint64Array.from(Buffer.from('234').toString())
};
expect(cleanJson(input)).toEqual(input);
});
});
});