feat(agent): add fuzz testing scripts for nostr schemas, DM decryptor, and magnet utils

This commit is contained in:
google-labs-jules[bot] committed 2026-01-26 16:53:30 +00:00
1 parent f2344a25d6
commit a1cb6377a6
5 files changed
+365 -503

No files matched your search

+60 -164
View File
@@ -1,179 +1,75 @@
import { decryptDM } from '../../js/dmDecryptor.js';
import {
fuzzBoolean,
fuzzInt,
fuzzString,
fuzzHexString,
fuzzJSON,
pickOne,
fuzzSurrogatePairString,
saveFuzzReport,
saveReproducer
} from './fuzz-utils.mjs';
import fs from 'fs';
import path from 'path';
const ITERATIONS = 1000;
const FINDINGS = [];
import fs from "fs";
import path from "path";
import { runFuzzer, rng } from "./fuzz-shared.mjs";
function saveReproducerDM(id, input, error) {
const reproDir = path.join('examples', 'reproducers', `fuzz-dmDecryptor-${id}`);
if (!fs.existsSync(reproDir)) {
fs.mkdirSync(reproDir, { recursive: true });
}
const originalPath = "../../js/dmDecryptor.js";
const tempPath = "./temp_dmDecryptor.mjs";
const readmeContent = `# Fuzz Reproducer: dmDecryptor - ${id}
// Read original file
const originalContent = fs.readFileSync(new URL(originalPath, import.meta.url), "utf-8");
## Error
\`\`\`
${error.stack || error.message}
\`\`\`
`;
// Replace import
// The original file has: import { normalizeActorKey } from "./nostr/watchHistory.js";
// We want to point to ./mocks/watchHistory.js
const patchedContent = originalContent.replace(
/import \{ normalizeActorKey \} from "\.\/nostr\/watchHistory\.js";/,
'import { normalizeActorKey } from "./mocks/watchHistory.js";'
);
fs.writeFileSync(path.join(reproDir, 'README.md'), readmeContent);
// Write temp file
fs.writeFileSync(new URL(tempPath, import.meta.url), patchedContent);
// Serialize input parts
const event = input.event;
const context = input.context;
// Import the patched module
const DmDecryptor = await import(tempPath);
// We can't easily capture the 'shouldFail' state unless we stored it on the object,
// but we can infer or just log it. For now, let's just save the structure.
const serializedContext = {
actorPubkey: context?.actorPubkey,
decryptorConfigs: context?.decryptors && Array.isArray(context.decryptors) ? context.decryptors.map(d => ({
scheme: d.scheme,
supportsGiftWrap: d.supportsGiftWrap,
})) : context?.decryptors
async function fuzzTest(iteration) {
const genEvent = () => {
return {
kind: rng.oneOf([4, 1059, rng.int(0, 20000)]),
pubkey: rng.mixedString(64),
created_at: rng.int(0, 2000000000),
content: rng.mixedString(100),
tags: rng.array(() => rng.array(() => rng.mixedString(20), 5), 5)
};
};
fs.writeFileSync(path.join(reproDir, 'input-event.json'), JSON.stringify(event, null, 2));
fs.writeFileSync(path.join(reproDir, 'input-context-config.json'), JSON.stringify(serializedContext, null, 2));
const genDecryptor = () => {
return {
scheme: rng.oneOf(["nip04", "nip44", "nip44_v2", rng.mixedString(10)]),
decrypt: async (pubkey, ciphertext) => {
if (rng.bool()) {
throw new Error("Decryption failed " + rng.mixedString(10));
}
// Return either a string (valid) or garbage
if (rng.bool()) {
return JSON.stringify({ content: "decrypted message" });
}
return rng.nastyString();
}
};
};
const scriptContent = `
import { decryptDM } from '../../../../js/dmDecryptor.js';
import fs from 'fs';
import path from 'path';
const event = rng.bool() ? genEvent() : rng.nastyString();
const context = {
actorPubkey: rng.mixedString(64),
decryptors: rng.array(genDecryptor, 3)
};
const event = JSON.parse(fs.readFileSync('input-event.json', 'utf8'));
const contextConfig = JSON.parse(fs.readFileSync('input-context-config.json', 'utf8'));
// Run the target
await DmDecryptor.decryptDM(event, context);
// Reconstruct decryptors
const decryptors = Array.isArray(contextConfig.decryptorConfigs)
? contextConfig.decryptorConfigs.map(config => ({
scheme: config.scheme,
supportsGiftWrap: config.supportsGiftWrap,
decrypt: async (pubkey, ciphertext) => {
console.log('Mock decrypt called for:', pubkey);
return "mock-decrypted-content";
}
}))
: contextConfig.decryptorConfigs;
const context = {
actorPubkey: contextConfig.actorPubkey,
decryptors
};
console.log('Running reproduction...');
try {
await decryptDM(event, context);
console.log('Execution finished without crash.');
} catch (error) {
console.error('Crash reproduced:', error);
}
`;
fs.writeFileSync(path.join(reproDir, 'repro.mjs'), scriptContent);
console.log(`Reproducer saved to ${reproDir}`);
return { event, context };
}
function logError(target, input, error) {
const id = Date.now().toString() + Math.floor(Math.random() * 1000);
console.error(`[FAIL] ${target} crashed! ID: ${id}`);
FINDINGS.push({
id,
target,
error: error.message,
stack: error.stack,
input
runFuzzer("dmDecryptor", 5000, fuzzTest)
.catch(err => {
console.error("Fatal fuzzer error:", err);
process.exit(1);
})
.finally(() => {
// Cleanup
if (fs.existsSync(new URL(tempPath, import.meta.url))) {
fs.unlinkSync(new URL(tempPath, import.meta.url));
}
});
saveReproducerDM(id, input, error);
}
function mockDecryptor(shouldFail = false) {
return {
scheme: pickOne(['nip04', 'nip44', 'nip44_v2', fuzzString()]),
decrypt: async (pubkey, ciphertext) => {
if (shouldFail) {
throw new Error('Mock Decryption Failed');
}
// Return a valid stringified JSON event sometimes, or random garbage
if (fuzzBoolean()) {
return JSON.stringify({
content: fuzzString(20),
pubkey: fuzzHexString(),
created_at: Math.floor(Date.now() / 1000),
tags: []
});
}
return fuzzString(50);
},
supportsGiftWrap: fuzzBoolean()
};
}
function generateRandomEvent() {
const kind = pickOne([4, 1059, fuzzInt(0, 20000)]);
const content = fuzzBoolean() ? fuzzString(100) : fuzzJSON(2);
return {
kind,
pubkey: fuzzHexString(),
created_at: Math.floor(Date.now() / 1000),
tags: fuzzBoolean() ? [] : [['p', fuzzHexString()], ['encrypted', 'nip44']],
content: typeof content === 'string' ? content : JSON.stringify(content)
};
}
function generateRandomContext() {
const decryptors = [];
const count = fuzzInt(0, 3);
for (let i = 0; i < count; i++) {
decryptors.push(mockDecryptor(fuzzBoolean())); // some will fail
}
return {
actorPubkey: fuzzHexString(),
decryptors
};
}
async function fuzzDecryptor() {
console.log('Starting fuzzing for dmDecryptor.js...');
for (let i = 0; i < ITERATIONS; i++) {
const event = generateRandomEvent();
const context = generateRandomContext();
// Also test null/undefined/garbage inputs
const inputEvent = pickOne([event, null, undefined, "garbage", {}]);
const inputContext = pickOne([context, null, undefined, {}, { decryptors: "garbage" }]);
try {
await decryptDM(inputEvent, inputContext);
} catch (error) {
logError('decryptDM', { event: inputEvent, context: inputContext }, error);
}
}
saveFuzzReport('dmDecryptor', FINDINGS);
console.log(`Fuzzing complete. Found ${FINDINGS.length} crashes.`);
if (FINDINGS.length > 0) {
process.exit(1);
}
}
fuzzDecryptor().catch(err => {
console.error('Fuzzing script failed:', err);
process.exit(1);
});
+37 -84
View File
@@ -1,90 +1,43 @@
import { normalizeAndAugmentMagnet } from '../../js/magnetUtils.js';
import {
fuzzBoolean,
fuzzInt,
fuzzString,
fuzzHexString,
fuzzJSON,
pickOne,
fuzzSurrogatePairString,
saveFuzzReport,
saveReproducer
} from './fuzz-utils.mjs';
const ITERATIONS = 1000;
const FINDINGS = [];
import { runFuzzer, rng } from "./fuzz-shared.mjs";
function logError(target, input, error) {
const id = Date.now().toString() + Math.floor(Math.random() * 1000);
console.error(`[FAIL] ${target} crashed! ID: ${id}`);
FINDINGS.push({
id,
target,
error: error.message,
stack: error.stack,
input
});
saveReproducer(target, id, input, error);
}
function generateRandomMagnet() {
const scheme = 'magnet:?';
const xt = `xt=urn:btih:${fuzzHexString(40)}`;
const dn = `dn=${fuzzString(10)}`;
const tr = `tr=${encodeURIComponent('wss://' + fuzzString(10) + '.com')}`;
// Mix valid parts with garbage
const parts = [xt, dn, tr];
if (fuzzBoolean()) parts.push(`ws=${encodeURIComponent('https://' + fuzzString(10) + '.com')}`);
if (fuzzBoolean()) parts.push(fuzzString(5) + '=' + fuzzString(5));
// Shuffle
parts.sort(() => Math.random() - 0.5);
return scheme + parts.join('&');
}
function generateRandomOptions() {
return {
webSeed: fuzzBoolean() ? [fuzzString()] : fuzzString(),
torrentUrl: fuzzBoolean() ? fuzzString() : undefined,
xs: fuzzBoolean() ? fuzzString() : undefined,
extraTrackers: fuzzBoolean() ? [fuzzString()] : [],
appProtocol: pickOne(['http:', 'https:', fuzzString(), undefined]),
logger: fuzzBoolean() ? () => {} : undefined
};
}
function fuzzMagnetUtils() {
console.log('Starting fuzzing for magnetUtils.js...');
for (let i = 0; i < ITERATIONS; i++) {
// Inputs: valid magnet, partial, garbage, huge string, percent encoded mess
const rawValue = pickOne([
generateRandomMagnet(),
fuzzString(100),
`magnet:?xt=${fuzzString(10)}`, // invalid xt
fuzzSurrogatePairString(50),
null,
undefined,
{},
123
]);
const options = fuzzBoolean() ? generateRandomOptions() : undefined;
try {
normalizeAndAugmentMagnet(rawValue, options);
} catch (error) {
logError('normalizeAndAugmentMagnet', { rawValue, options }, error);
}
// Polyfills
global.window = {
location: {
protocol: "https:"
}
};
saveFuzzReport('magnetUtils', FINDINGS);
console.log(`Fuzzing complete. Found ${FINDINGS.length} crashes.`);
if (FINDINGS.length > 0) {
process.exit(1);
}
// Import the target module
// Note: We are in scripts/agent/
import * as MagnetUtils from "../../js/magnetUtils.js";
async function fuzzTest(iteration) {
// Randomize window protocol sometimes
global.window.location.protocol = rng.bool() ? "https:" : "http:";
const rawValue = rng.mixedString(100);
const genOptions = () => {
if (rng.bool()) return null;
return {
webSeed: rng.bool() ? rng.mixedString(50) : rng.array(() => rng.mixedString(50), 3),
torrentUrl: rng.mixedString(50),
xs: rng.mixedString(50),
extraTrackers: rng.array(() => rng.mixedString(30), 5),
logger: rng.bool() ? (() => {}) : null,
appProtocol: rng.bool() ? rng.oneOf(["http:", "https:", rng.nastyString()]) : undefined
};
};
const options = genOptions();
MagnetUtils.normalizeAndAugmentMagnet(rawValue, options);
return { rawValue, options };
}
fuzzMagnetUtils();
runFuzzer("magnetUtils", 5000, fuzzTest).catch(err => {
console.error("Fatal fuzzer error:", err);
process.exit(1);
});
+113 -160
View File
@@ -1,173 +1,126 @@
import {
buildVideoPostEvent,
buildHttpAuthEvent,
buildReportEvent,
buildVideoMirrorEvent,
buildRepostEvent,
buildShareEvent,
buildRelayListEvent,
buildDmRelayListEvent,
buildProfileMetadataEvent,
buildMuteListEvent,
buildDeletionEvent,
buildLegacyDirectMessageEvent,
buildDmAttachmentEvent,
buildDmReadReceiptEvent,
buildDmTypingIndicatorEvent,
buildViewEvent,
buildZapRequestEvent,
buildReactionEvent,
buildCommentEvent,
buildWatchHistoryEvent,
buildSubscriptionListEvent,
buildBlockListEvent,
buildHashtagPreferenceEvent,
buildAdminListEvent,
validateEventStructure,
sanitizeAdditionalTags,
NOTE_TYPES
} from '../../js/nostrEventSchemas.js';
import {
fuzzBoolean,
fuzzInt,
fuzzString,
fuzzHexString,
fuzzJSON,
pickOne,
fuzzSurrogatePairString,
saveFuzzReport,
saveReproducer
} from './fuzz-utils.mjs';
import { runFuzzer, rng } from "./fuzz-shared.mjs";
import { TextEncoder, TextDecoder } from "util";
const ITERATIONS = 1000;
const FINDINGS = [];
// Polyfills
global.TextEncoder = TextEncoder;
global.TextDecoder = TextDecoder;
global.window = {
bitvidNostrEventOverrides: {}
};
function logError(target, input, error) {
const id = Date.now().toString() + Math.floor(Math.random() * 1000);
console.error(`[FAIL] ${target} crashed! ID: ${id}`);
FINDINGS.push({
id,
target,
error: error.message,
stack: error.stack,
input
});
saveReproducer(target, id, input, error);
}
// Import the target module
// Note: We are in scripts/agent/
import * as Schemas from "../../js/nostrEventSchemas.js";
function generateRandomParams() {
const params = {};
const keys = ['pubkey', 'created_at', 'content', 'tags', 'additionalTags', 'dTagValue', 'url', 'method', 'payload', 'eventId', 'userId', 'reportType', 'relayHint', 'recipientPubkey', 'ciphertext', 'attachment', 'messageKind', 'expiresAt', 'pointerValue', 'pointerTag', 'dedupeTag', 'amountSats', 'lnurl', 'coordinate', 'videoEventId', 'rootKind'];
async function fuzzTest(iteration) {
// We choose a random function to fuzz in each iteration or fuzz all of them?
// Let's pick one random target per iteration to keep it simple and isolate crashes.
// Randomly select some keys to populate
keys.forEach(key => {
if (fuzzBoolean()) {
if (key === 'created_at' || key === 'amountSats' || key === 'messageKind' || key === 'expiresAt') {
params[key] = fuzzBoolean() ? fuzzInt() : fuzzString(); // Mix types
} else if (key === 'pubkey' || key === 'recipientPubkey') {
params[key] = fuzzBoolean() ? fuzzHexString() : fuzzString();
} else if (key === 'tags' || key === 'additionalTags') {
params[key] = fuzzBoolean() ? [] : fuzzJSON(2); // Can be valid array or random JSON
} else if (key === 'content') {
params[key] = fuzzBoolean() ? fuzzSurrogatePairString(100) : fuzzJSON(2);
} else {
params[key] = fuzzString();
}
}
});
const targets = [
"buildVideoPostEvent",
"buildViewEvent",
"buildCommentEvent",
"buildReactionEvent",
"buildZapRequestEvent",
"sanitizeAdditionalTags",
"validateEventStructure"
];
return params;
}
const targetName = rng.oneOf(targets);
let inputs = {};
function fuzzBuilder(name, builderFn, argsGenerator) {
for (let i = 0; i < ITERATIONS; i++) {
const args = argsGenerator();
try {
if (Array.isArray(args)) {
builderFn(...args);
} else {
builderFn(args);
}
} catch (error) {
logError(name, args, error);
}
}
}
// Helper to generate a comprehensive random params object
const genParams = () => {
// 50% chance of being undefined/null/non-object to test robustness
if (Math.random() < 0.1) return rng.nastyString();
if (Math.random() < 0.1) return null;
console.log('Starting fuzzing for nostrEventSchemas.js...');
// Otherwise generate a rich object
const params = {
pubkey: rng.mixedString(64),
created_at: rng.bool() ? rng.int(0, 2000000000) : rng.nastyString(),
content: rng.bool() ? rng.mixedString(1000) : rng.recursiveObject(2),
additionalTags: rng.bool() ? rng.array(() => rng.array(() => rng.mixedString(20), 5), 10) : rng.nastyString(),
};
// Fuzz all builders
const builders = [
{ name: 'buildVideoPostEvent', fn: buildVideoPostEvent },
{ name: 'buildHttpAuthEvent', fn: buildHttpAuthEvent },
{ name: 'buildReportEvent', fn: buildReportEvent },
{ name: 'buildVideoMirrorEvent', fn: buildVideoMirrorEvent },
{ name: 'buildRepostEvent', fn: buildRepostEvent },
{ name: 'buildShareEvent', fn: buildShareEvent },
{ name: 'buildRelayListEvent', fn: buildRelayListEvent },
{ name: 'buildDmRelayListEvent', fn: buildDmRelayListEvent },
{ name: 'buildProfileMetadataEvent', fn: buildProfileMetadataEvent },
{ name: 'buildMuteListEvent', fn: buildMuteListEvent },
{ name: 'buildDeletionEvent', fn: buildDeletionEvent },
{ name: 'buildLegacyDirectMessageEvent', fn: buildLegacyDirectMessageEvent },
{ name: 'buildDmAttachmentEvent', fn: buildDmAttachmentEvent },
{ name: 'buildDmReadReceiptEvent', fn: buildDmReadReceiptEvent },
{ name: 'buildDmTypingIndicatorEvent', fn: buildDmTypingIndicatorEvent },
{ name: 'buildViewEvent', fn: buildViewEvent },
{ name: 'buildZapRequestEvent', fn: buildZapRequestEvent },
{ name: 'buildReactionEvent', fn: buildReactionEvent },
{ name: 'buildCommentEvent', fn: buildCommentEvent },
{ name: 'buildWatchHistoryEvent', fn: buildWatchHistoryEvent },
{ name: 'buildSubscriptionListEvent', fn: buildSubscriptionListEvent },
{ name: 'buildBlockListEvent', fn: buildBlockListEvent },
{ name: 'buildHashtagPreferenceEvent', fn: buildHashtagPreferenceEvent },
];
// Add specific fields that might be used by specific builders
const extraFields = [
"dTagValue", "url", "magnet", "thumbnail", "description", "mode", "videoRootId",
"pointerValue", "pointerTag", "pointerTags", "dedupeTag", "includeSessionTag",
"videoEventId", "videoEventRelay", "videoDefinitionAddress", "rootIdentifier",
"parentCommentId", "rootKind", "rootAuthorPubkey", "parentKind",
"recipientPubkey", "relays", "amountSats", "lnurl", "eventId", "coordinate",
"targetPointer", "targetAuthorPubkey"
];
builders.forEach(({ name, fn }) => {
fuzzBuilder(name, fn, () => [generateRandomParams()]);
});
// Special case for buildAdminListEvent
fuzzBuilder('buildAdminListEvent', buildAdminListEvent, () => {
const listKey = pickOne(['moderation', 'editors', 'whitelist', 'blacklist', fuzzString()]);
return [listKey, generateRandomParams()];
});
// Fuzz validateEventStructure
fuzzBuilder('validateEventStructure', validateEventStructure, () => {
const type = pickOne([...Object.values(NOTE_TYPES), fuzzString()]);
const event = fuzzJSON(2); // Random object/JSON
return [type, event];
});
// Fuzz sanitizeAdditionalTags
fuzzBuilder('sanitizeAdditionalTags', sanitizeAdditionalTags, () => {
// Random input: array of arrays, array of strings, random object, etc.
if (fuzzBoolean()) {
// structured but potentially malformed tags
const tags = [];
const count = fuzzInt(0, 10);
for (let i = 0; i < count; i++) {
if (fuzzBoolean()) {
const tag = [fuzzString()];
const args = fuzzInt(0, 5);
for (let j = 0; j < args; j++) {
tag.push(fuzzBoolean() ? fuzzString() : fuzzJSON(1));
extraFields.forEach(field => {
if (rng.bool()) {
params[field] = rng.mixedString(50);
} else if (rng.bool()) {
params[field] = rng.recursiveObject(1); // Potentially invalid type
}
tags.push(tag);
} else {
tags.push(pickOne([null, undefined, 123, {}, "string"]));
}
}
return [tags];
}
// completely random input
return [fuzzJSON(2)];
});
});
saveFuzzReport('nostrEventSchemas', FINDINGS);
console.log(`Fuzzing complete. Found ${FINDINGS.length} crashes.`);
if (FINDINGS.length > 0) {
process.exit(1);
return params;
};
switch (targetName) {
case "buildVideoPostEvent":
inputs = genParams();
Schemas.buildVideoPostEvent(inputs);
break;
case "buildViewEvent":
inputs = genParams();
Schemas.buildViewEvent(inputs);
break;
case "buildCommentEvent":
inputs = genParams();
Schemas.buildCommentEvent(inputs);
break;
case "buildReactionEvent":
inputs = genParams();
Schemas.buildReactionEvent(inputs);
break;
case "buildZapRequestEvent":
inputs = genParams();
Schemas.buildZapRequestEvent(inputs);
break;
case "sanitizeAdditionalTags":
// sanitizeAdditionalTags expects an array of tags (arrays)
if (rng.bool()) {
inputs = rng.array(() => rng.array(() => rng.mixedString(20), 5), 20);
} else {
inputs = rng.nastyString(); // or object, etc.
}
Schemas.sanitizeAdditionalTags(inputs);
break;
case "validateEventStructure":
const type = rng.oneOf(Object.values(Schemas.NOTE_TYPES));
let event = genParams(); // repurpose genParams to make a fake event
// Ensure event is an object for this specific test setup, unless we want to fuzz that too.
// validateEventStructure checks: if (!event || typeof event !== "object") return { valid: false ... }
// So passing a string is valid input for the function (should not crash),
// but my fuzzer crashes trying to assign .kind to it.
if (event && typeof event === 'object') {
event.kind = rng.int(0, 50000);
event.tags = rng.array(() => rng.array(() => rng.mixedString(10), 3), 5);
}
inputs = { type, event };
Schemas.validateEventStructure(type, event);
break;
}
return { target: targetName, inputs };
}
runFuzzer("nostrEventSchemas", 5000, fuzzTest).catch(err => {
console.error("Fatal fuzzer error:", err);
process.exit(1);
});
+149 -95
View File
@@ -5,133 +5,187 @@ import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const REPRODUCERS_DIR = path.resolve(__dirname, "../../examples/reproducers");
const ARTIFACTS_DIR = path.resolve(__dirname, "../../artifacts");
const REPRODUCER_DIR = path.join(process.cwd(), "examples/reproducers");
const ARTIFACTS_DIR = path.join(process.cwd(), "artifacts");
// Ensure directories exist
if (!fs.existsSync(REPRODUCERS_DIR)) {
fs.mkdirSync(REPRODUCERS_DIR, { recursive: true });
if (!fs.existsSync(REPRODUCER_DIR)) {
fs.mkdirSync(REPRODUCER_DIR, { recursive: true });
}
if (!fs.existsSync(ARTIFACTS_DIR)) {
fs.mkdirSync(ARTIFACTS_DIR, { recursive: true });
}
export function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
export const rng = {
bool: () => Math.random() > 0.5,
int: (min = 0, max = 100) => Math.floor(Math.random() * (max - min + 1)) + min,
float: (min = 0, max = 100) => Math.random() * (max - min) + min,
oneOf: (arr) => arr[Math.floor(Math.random() * arr.length)],
export function randomString(length) {
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+{}|:<>?~`-=[]\\;',./";
let result = "";
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
export function randomHex(length) {
const chars = "0123456789abcdef";
let result = "";
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
export function randomBoolean() {
return Math.random() < 0.5;
}
export function randomItem(array) {
return array[Math.floor(Math.random() * array.length)];
}
export function randomJSON(depth = 3, maxFields = 5) {
if (depth <= 0) {
const type = randomInt(0, 3);
if (type === 0) return randomString(10);
if (type === 1) return randomInt(-1000, 1000);
if (type === 2) return randomBoolean();
return null;
}
const type = randomInt(0, 5);
if (type === 0) {
// Array
const arr = [];
const len = randomInt(0, maxFields);
string: (maxLength = 100) => {
const len = Math.floor(Math.random() * maxLength);
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
for (let i = 0; i < len; i++) {
arr.push(randomJSON(depth - 1, maxFields));
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return arr;
} else if (type === 1) {
// Object
const obj = {};
const len = randomInt(0, maxFields);
return result;
},
nastyString: () => {
const candidates = [
"",
"null",
"undefined",
"NaN",
"\x00",
"\n",
"\r",
"\t",
" ",
" ",
"https://example.com",
"wss://relay.example.com",
"<script>alert(1)</script>",
"' OR 1=1 --",
"javascript:void(0)",
"data:text/plain;base64,SGVsbG8=",
"😀",
"ñ",
"\uD83D\uDCA9", // Pile of poo
"\uD800", // Lone surrogate (high)
"\uDFFF", // Lone surrogate (low)
"A".repeat(1000), // Long string
"A".repeat(10000), // Very long string
JSON.stringify({ foo: "bar" }),
"{}",
"[]"
];
return candidates[Math.floor(Math.random() * candidates.length)];
},
mixedString: (maxLength = 100) => {
if (Math.random() < 0.3) return rng.nastyString();
return rng.string(maxLength);
},
array: (generator, maxLen = 10) => {
const len = rng.int(0, maxLen);
const result = [];
for (let i = 0; i < len; i++) {
obj[randomString(5)] = randomJSON(depth - 1, maxFields);
result.push(generator());
}
return result;
},
object: (keyGen, valGen, maxKeys = 10) => {
const len = rng.int(0, maxKeys);
const result = {};
for (let i = 0; i < len; i++) {
result[keyGen()] = valGen();
}
return result;
},
recursiveObject: (depth = 3) => {
if (depth <= 0) return rng.mixedString();
const type = rng.int(0, 4);
if (type === 0) return rng.int(-1000, 1000);
if (type === 1) return rng.mixedString();
if (type === 2) return rng.bool();
if (type === 3) return null;
if (type === 4) {
// Array
const len = rng.int(0, 5);
const arr = [];
for(let i=0; i<len; i++) arr.push(rng.recursiveObject(depth - 1));
return arr;
}
// Object
const len = rng.int(0, 5);
const obj = {};
for(let i=0; i<len; i++) {
obj[rng.string(10)] = rng.recursiveObject(depth - 1);
}
return obj;
} else {
// Scalar
return randomJSON(0);
}
}
};
export function saveReproducer(target, input, error) {
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const filename = `crash-${target}-${timestamp}.json`;
const filepath = path.join(REPRODUCERS_DIR, filename);
export function saveReproducer(targetName, input, error) {
const timestamp = Date.now();
const filename = `fuzz-${targetName}-${timestamp}.json`;
const filepath = path.join(REPRODUCER_DIR, filename);
const report = {
target,
const content = {
target: targetName,
timestamp: new Date().toISOString(),
error: {
message: error.message,
stack: error.stack,
name: error.name
message: error.message,
stack: error.stack,
code: error.code,
name: error.name
},
input
};
fs.writeFileSync(filepath, JSON.stringify(report, null, 2));
fs.writeFileSync(filepath, JSON.stringify(content, null, 2));
console.error(`[FUZZ] Saved reproducer to ${filepath}`);
return filename;
return filepath;
}
export async function runFuzzer(name, fuzzFn, iterations = 1000) {
console.log(`[FUZZ] Starting fuzzer for ${name} with ${iterations} iterations...`);
const crashes = [];
const start = Date.now();
export function saveReport(targetName, stats) {
const filepath = path.join(ARTIFACTS_DIR, `fuzz-report-${targetName}.json`);
fs.writeFileSync(filepath, JSON.stringify(stats, null, 2));
console.log(`[FUZZ] Saved report to ${filepath}`);
}
export async function runFuzzer(targetName, iterations, testFn) {
console.log(`[FUZZ] Starting fuzzer for ${targetName} with ${iterations} iterations...`);
const stats = {
target: targetName,
iterations: 0,
passed: 0,
failed: 0,
crashes: [], // List of { input, error, reproducerPath }
startTime: Date.now(),
endTime: null
};
for (let i = 0; i < iterations; i++) {
if (i % 100 === 0) process.stdout.write(".");
let currentInput = null;
stats.iterations++;
let input = null;
try {
const input = await fuzzFn(i);
currentInput = input;
} catch (error) {
process.stdout.write("E");
const filename = saveReproducer(name, currentInput, error);
crashes.push({
input = await testFn(i);
stats.passed++;
} catch (err) {
stats.failed++;
const reproducerPath = saveReproducer(targetName, input, err);
stats.crashes.push({
iteration: i,
error: error.message,
reproducer: filename
error: err.message,
reproducerPath
});
// Optionally stop on first crash or continue
// For now, we continue but log it.
}
if (i % 100 === 0 && i > 0) {
process.stdout.write(".");
}
}
process.stdout.write("\n");
console.log("\n");
const duration = (Date.now() - start) / 1000;
console.log(`[FUZZ] Finished ${name} in ${duration.toFixed(2)}s. Crashes: ${crashes.length}`);
stats.endTime = Date.now();
stats.duration = stats.endTime - stats.startTime;
const reportPath = path.join(ARTIFACTS_DIR, `fuzz-report-${name}.json`);
const report = {
target: name,
iterations,
duration,
crashes
};
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log(`[FUZZ] Saved report to ${reportPath}`);
console.log(`[FUZZ] Finished. ${stats.passed} passed, ${stats.failed} failed.`);
saveReport(targetName, stats);
return stats;
}
+6
View File
@@ -0,0 +1,6 @@
export function normalizeActorKey(key) {
if (typeof key !== 'string') return '';
// simple hex check or return as is for mocking
return key.toLowerCase().trim();
}