mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
feat: add fuzz testing infrastructure and fix nostrEventSchemas crash
- Added `scripts/agent/fuzz-schemas.mjs`, `fuzz-dm.mjs`, `fuzz-magnet.mjs` and `fuzz-setup.mjs`. - Fixed crash in `js/nostrEventSchemas.js` (buildAdminListEvent log and getNostrEventSchema type check). - Added crash reproducers in `examples/reproducers/`. - Fuzzers output reports to `artifacts/`. - Updated fuzzers to deduplicate crashes and safely stringify inputs.
This commit is contained in:
1 parent
8b1358a619
commit
241ad32dc4
8 files changed
+567
-1
No files matched your search
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"function": "buildAdminListEvent",
|
||||
"input": "{\"listKey\":{},\"address\":\"Ꟁ뤷世㙭硇ఴ冸\\uddc2\\ud9ac謢\\udd5f逎맗\\ud97dᢈ哏⟃\\udb4d佢\",\"pubkey\":\"__undefined__\",\"rootKind\":\":c+p/*=7^=GB_j>Hhh]56YW&ZF$28%u/J5d0_\",\"eventId\":63930,\"includeSessionTag\":76813,\"pointerValue\":\"\"}",
|
||||
"error": "Error: toString throw",
|
||||
"stack": "Error: toString throw\n at Object.toString (file:///app/scripts/agent/fuzz-schemas.mjs:83:31)\n at Module.buildAdminListEvent (file:///app/js/nostrEventSchemas.js:2296:54)\n at fn (file:///app/scripts/agent/fuzz-schemas.mjs:152:64)\n at run (file:///app/scripts/agent/fuzz-schemas.mjs:167:32)\n at file:///app/scripts/agent/fuzz-schemas.mjs:216:1"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"function": "buildAdminListEvent",
|
||||
"input": "{\"repostKind\":true,\"listKey\":{},\"content\":false,\"q?F#3\":true}",
|
||||
"error": "Error: toString throw",
|
||||
"stack": "Error: toString throw\n at Object.toString (file:///app/scripts/agent/fuzz-schemas.mjs:83:31)\n at Module.buildAdminListEvent (file:///app/js/nostrEventSchemas.js:2296:54)\n at fn (file:///app/scripts/agent/fuzz-schemas.mjs:152:64)\n at run (file:///app/scripts/agent/fuzz-schemas.mjs:167:32)\n at file:///app/scripts/agent/fuzz-schemas.mjs:216:1"
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"function": "buildAdminListEvent",
|
||||
"input": "{\"threadParticipantPubkey\":\"=\\\"-ge5\",\"includeSessionTag\":\"C2@IUQ8DFi3\",\"address\":\"\\udab6ꇷ뻼웖\\ude2d\\udf2c䙹轂岀ꊀ魯㣝»ヒ趃㩎떡\\ude0d얭⿴灎쳽幔箆⋙Ё⥩덙\",\"ciphertext\":{\"bQyO]\":null,\"6<w,>\":false},\"listKey\":{},\"Xx)&~\":\"䍖ᔩ㒪暸╝鋶簇ᯢ⽷\\udd40察畂캌\\udc5d\\uda52魾웁尢穁鶽ߒ\\uda6c諉┯쭈蟡㿏ࢬ\\udcdf\\udea9㺖\"}",
|
||||
"error": "Error: toString throw",
|
||||
"stack": "Error: toString throw\n at Object.toString (file:///app/scripts/agent/fuzz-schemas.mjs:83:31)\n at Module.buildAdminListEvent (file:///app/js/nostrEventSchemas.js:2296:54)\n at fn (file:///app/scripts/agent/fuzz-schemas.mjs:152:64)\n at run (file:///app/scripts/agent/fuzz-schemas.mjs:167:32)\n at file:///app/scripts/agent/fuzz-schemas.mjs:216:1"
|
||||
}
|
||||
@@ -663,6 +663,9 @@ export function setNostrEventSchemaOverrides(overrides = {}) {
|
||||
}
|
||||
|
||||
export function getNostrEventSchema(type) {
|
||||
if (typeof type !== "string") {
|
||||
return null;
|
||||
}
|
||||
const base = BASE_SCHEMAS[type];
|
||||
if (!base) {
|
||||
return null;
|
||||
@@ -2293,7 +2296,7 @@ export function buildAdminListEvent(listKey, params) {
|
||||
const schema = getNostrEventSchema(resolveAdminNoteType(listKey));
|
||||
if (!schema) {
|
||||
devLogger.warn(
|
||||
`[nostrEventSchemas] Unknown admin list key: ${listKey}`,
|
||||
`[nostrEventSchemas] Unknown admin list key: ${safeStringify(listKey)}`,
|
||||
);
|
||||
return {
|
||||
kind: 30000,
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import './fuzz-setup.mjs';
|
||||
import * as dm from '../../js/dmDecryptor.js';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
const REPRODUCER_DIR = 'examples/reproducers';
|
||||
const REPORT_FILE = 'artifacts/fuzz-report-dm.json';
|
||||
|
||||
const ITERATIONS = 5000;
|
||||
|
||||
// Verify export
|
||||
if (typeof dm.decryptDM !== 'function') {
|
||||
console.error("Error: decryptDM is not exported from js/dmDecryptor.js");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Random generators
|
||||
function getRandomInt(max) {
|
||||
return Math.floor(Math.random() * max);
|
||||
}
|
||||
|
||||
function getRandomString(length = 100) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getRandomHex(length = 64) {
|
||||
const chars = '0123456789abcdef';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getRandomTags() {
|
||||
const numTags = getRandomInt(5);
|
||||
const tags = [];
|
||||
for(let i=0; i<numTags; i++) {
|
||||
const tagLen = getRandomInt(4) + 1;
|
||||
const tag = [];
|
||||
tag.push(Math.random() > 0.5 ? 'p' : (Math.random() > 0.5 ? 'encrypted' : getRandomString(5)));
|
||||
for(let j=1; j<tagLen; j++) {
|
||||
tag.push(Math.random() > 0.8 ? getRandomHex(64) : getRandomString(20));
|
||||
}
|
||||
tags.push(tag);
|
||||
}
|
||||
// inject malformed tags
|
||||
if (Math.random() < 0.1) tags.push("not-an-array");
|
||||
if (Math.random() < 0.1) tags.push([]);
|
||||
return tags;
|
||||
}
|
||||
|
||||
async function run() {
|
||||
console.log(`Starting fuzzing DM decryptor for ${ITERATIONS} iterations...`);
|
||||
|
||||
const report = {
|
||||
target: 'js/dmDecryptor.js',
|
||||
timestamp: new Date().toISOString(),
|
||||
iterations: ITERATIONS,
|
||||
crashes: [],
|
||||
};
|
||||
|
||||
const uniqueCrashes = new Set();
|
||||
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
const event = {
|
||||
kind: Math.random() > 0.5 ? 4 : 1059,
|
||||
pubkey: getRandomHex(64),
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: getRandomTags(),
|
||||
content: getRandomString(50),
|
||||
};
|
||||
|
||||
// Malform event
|
||||
if (Math.random() < 0.1) event.kind = "string";
|
||||
if (Math.random() < 0.1) event.tags = null;
|
||||
if (Math.random() < 0.1) event.content = null;
|
||||
|
||||
const context = {
|
||||
actorPubkey: getRandomHex(64),
|
||||
decryptors: [
|
||||
{
|
||||
scheme: 'nip44',
|
||||
decrypt: async (pk, ciphertext) => {
|
||||
if (Math.random() < 0.1) throw new Error("Decrypt failed");
|
||||
if (Math.random() < 0.1) return null;
|
||||
return "decrypted-content";
|
||||
},
|
||||
priority: 1
|
||||
},
|
||||
{
|
||||
scheme: 'nip04',
|
||||
decrypt: async () => "legacy-decrypted",
|
||||
priority: 0
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
// Malform context
|
||||
if (Math.random() < 0.1) context.decryptors = null;
|
||||
if (Math.random() < 0.1) context.decryptors = [null, {}];
|
||||
|
||||
try {
|
||||
await dm.decryptDM(event, context);
|
||||
} catch (err) {
|
||||
const crashKey = `decryptDM:${err.message}`;
|
||||
if (uniqueCrashes.has(crashKey)) {
|
||||
continue;
|
||||
}
|
||||
uniqueCrashes.add(crashKey);
|
||||
|
||||
let safeInput = "Could not stringify input";
|
||||
try {
|
||||
safeInput = JSON.stringify({ event, context: { ...context, decryptors: 'mocked' } });
|
||||
} catch (e) {
|
||||
safeInput = "[Input causes JSON.stringify to throw]";
|
||||
}
|
||||
|
||||
const crashInfo = {
|
||||
function: 'decryptDM',
|
||||
input: safeInput,
|
||||
error: err.toString(),
|
||||
stack: err.stack,
|
||||
};
|
||||
|
||||
const hash = crypto.createHash('sha256').update(JSON.stringify(crashInfo)).digest('hex').slice(0, 8);
|
||||
const filename = `crash-dm-${hash}.json`;
|
||||
fs.writeFileSync(path.join(REPRODUCER_DIR, filename), JSON.stringify(crashInfo, null, 2));
|
||||
|
||||
report.crashes.push({
|
||||
file: filename,
|
||||
function: 'decryptDM',
|
||||
error: err.message
|
||||
});
|
||||
|
||||
console.error(`[CRASH] decryptDM failed! Saved to ${filename}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(path.dirname(REPORT_FILE))) {
|
||||
fs.mkdirSync(path.dirname(REPORT_FILE), { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(REPORT_FILE, JSON.stringify(report, null, 2));
|
||||
console.log(`Fuzzing complete. Report saved to ${REPORT_FILE}. Crashes: ${report.crashes.length}`);
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,120 @@
|
||||
import './fuzz-setup.mjs';
|
||||
import * as magnetUtils from '../../js/magnetUtils.js';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
const REPRODUCER_DIR = 'examples/reproducers';
|
||||
const REPORT_FILE = 'artifacts/fuzz-report-magnet.json';
|
||||
|
||||
const ITERATIONS = 5000;
|
||||
|
||||
function getRandomString(length = 100) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+{}|:"<>?~`-=[]\;\',./% ';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getRandomMagnet() {
|
||||
const hash = getRandomString(40).replace(/[^0-9a-f]/gi, 'a');
|
||||
return `magnet:?xt=urn:btih:${hash}&dn=${getRandomString(10)}&tr=${encodeURIComponent(getRandomString(20))}`;
|
||||
}
|
||||
|
||||
async function run() {
|
||||
console.log(`Starting fuzzing magnet utils for ${ITERATIONS} iterations...`);
|
||||
|
||||
const report = {
|
||||
target: 'js/magnetUtils.js',
|
||||
timestamp: new Date().toISOString(),
|
||||
iterations: ITERATIONS,
|
||||
crashes: [],
|
||||
};
|
||||
|
||||
const uniqueCrashes = new Set();
|
||||
|
||||
const targets = [
|
||||
{ name: 'normalizeAndAugmentMagnet', fn: magnetUtils.normalizeAndAugmentMagnet, args: 2 },
|
||||
{ name: 'safeDecodeMagnet', fn: magnetUtils.safeDecodeMagnet, args: 1 },
|
||||
];
|
||||
|
||||
for (const { name, fn, args } of targets) {
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
let input, options;
|
||||
|
||||
if (Math.random() < 0.3) {
|
||||
input = getRandomMagnet();
|
||||
} else if (Math.random() < 0.3) {
|
||||
input = getRandomString(getRandomInt(200));
|
||||
} else {
|
||||
input = null; // or undefined, or object
|
||||
if (Math.random() < 0.5) input = {};
|
||||
}
|
||||
|
||||
if (args > 1) {
|
||||
options = {
|
||||
webSeed: Math.random() < 0.5 ? getRandomString(20) : [getRandomString(20)],
|
||||
torrentUrl: getRandomString(20),
|
||||
extraTrackers: [getRandomString(20)],
|
||||
appProtocol: 'https:',
|
||||
};
|
||||
if (Math.random() < 0.1) options = null;
|
||||
if (Math.random() < 0.1) options = "string";
|
||||
}
|
||||
|
||||
try {
|
||||
if (args === 1) {
|
||||
fn(input);
|
||||
} else {
|
||||
fn(input, options);
|
||||
}
|
||||
} catch (err) {
|
||||
const crashKey = `${name}:${err.message}`;
|
||||
if (uniqueCrashes.has(crashKey)) {
|
||||
continue;
|
||||
}
|
||||
uniqueCrashes.add(crashKey);
|
||||
|
||||
let safeInput = "Could not stringify input";
|
||||
try {
|
||||
safeInput = JSON.stringify(args === 1 ? input : { input, options });
|
||||
} catch (e) {
|
||||
safeInput = "[Input causes JSON.stringify to throw]";
|
||||
}
|
||||
|
||||
const crashInfo = {
|
||||
function: name,
|
||||
input: safeInput,
|
||||
error: err.toString(),
|
||||
stack: err.stack,
|
||||
};
|
||||
|
||||
const hash = crypto.createHash('sha256').update(JSON.stringify(crashInfo)).digest('hex').slice(0, 8);
|
||||
const filename = `crash-magnet-${name}-${hash}.json`;
|
||||
fs.writeFileSync(path.join(REPRODUCER_DIR, filename), JSON.stringify(crashInfo, null, 2));
|
||||
|
||||
report.crashes.push({
|
||||
file: filename,
|
||||
function: name,
|
||||
error: err.message
|
||||
});
|
||||
|
||||
console.error(`[CRASH] ${name} failed! Saved to ${filename}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(path.dirname(REPORT_FILE))) {
|
||||
fs.mkdirSync(path.dirname(REPORT_FILE), { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(REPORT_FILE, JSON.stringify(report, null, 2));
|
||||
console.log(`Fuzzing complete. Report saved to ${REPORT_FILE}. Crashes: ${report.crashes.length}`);
|
||||
}
|
||||
|
||||
function getRandomInt(max) {
|
||||
return Math.floor(Math.random() * max);
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,217 @@
|
||||
import './fuzz-setup.mjs';
|
||||
import * as schemas from '../../js/nostrEventSchemas.js';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
const REPRODUCER_DIR = 'examples/reproducers';
|
||||
const REPORT_FILE = 'artifacts/fuzz-report-schemas.json';
|
||||
|
||||
const ITERATIONS = 5000;
|
||||
|
||||
// Fuzzing inputs generator
|
||||
function getRandomInt(max) {
|
||||
return Math.floor(Math.random() * max);
|
||||
}
|
||||
|
||||
function getRandomString(length = 100) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+{}|:"<>?~`-=[]\;\',./';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getRandomUnicodeString(length = 100) {
|
||||
let result = '';
|
||||
// Include surrogates and other edge cases
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (Math.random() < 0.1) {
|
||||
// Surrogate pair halves
|
||||
result += String.fromCharCode(0xD800 + Math.random() * 0x800);
|
||||
} else {
|
||||
result += String.fromCharCode(Math.random() * 0xFFFF);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getRandomValue(depth = 0) {
|
||||
if (depth > 3) return null;
|
||||
const type = Math.random();
|
||||
if (type < 0.1) return null;
|
||||
if (type < 0.2) return undefined;
|
||||
if (type < 0.3) return getRandomInt(100000);
|
||||
if (type < 0.5) return getRandomString(getRandomInt(50));
|
||||
if (type < 0.6) return getRandomUnicodeString(getRandomInt(50));
|
||||
if (type < 0.7) return true;
|
||||
if (type < 0.8) return false;
|
||||
if (type < 0.9) {
|
||||
const arr = [];
|
||||
const len = getRandomInt(5);
|
||||
for(let i=0; i<len; i++) arr.push(getRandomValue(depth + 1));
|
||||
return arr;
|
||||
}
|
||||
const obj = {};
|
||||
const len = getRandomInt(5);
|
||||
for(let i=0; i<len; i++) {
|
||||
obj[getRandomString(5)] = getRandomValue(depth + 1);
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
const interestingValues = [
|
||||
null,
|
||||
undefined,
|
||||
0,
|
||||
1,
|
||||
-1,
|
||||
Infinity,
|
||||
-Infinity,
|
||||
NaN,
|
||||
"",
|
||||
" ",
|
||||
"\n",
|
||||
"\t",
|
||||
"{}",
|
||||
"[]",
|
||||
"true",
|
||||
"false",
|
||||
"null",
|
||||
"undefined",
|
||||
{ toString: () => { throw new Error('toString throw'); } },
|
||||
{ toJSON: () => { throw new Error('toJSON throw'); } },
|
||||
new Array(1000).fill('a').join(''), // large string
|
||||
"javascript:alert(1)",
|
||||
"__proto__",
|
||||
"constructor",
|
||||
];
|
||||
|
||||
function generateFuzzInput(paramsSchema) {
|
||||
// If we knew the schema of params, we could be smarter.
|
||||
// For now, generate a random object with random keys, plus keys that likely exist.
|
||||
const input = {};
|
||||
const commonKeys = [
|
||||
'pubkey', 'created_at', 'content', 'tags', 'additionalTags',
|
||||
'id', 'kind', 'dTagValue', 'eventId', 'eventRelay', 'address',
|
||||
'addressRelay', 'authorPubkey', 'repostKind', 'targetKind',
|
||||
'targetEvent', 'serializedEvent', 'video', 'relays', 'metadata',
|
||||
'pTags', 'eventIds', 'addresses', 'reason', 'recipientPubkey',
|
||||
'ciphertext', 'attachment', 'messageKind', 'expiresAt',
|
||||
'pointerValue', 'pointerTag', 'pointerTags', 'dedupeTag',
|
||||
'includeSessionTag', 'amountSats', 'lnurl', 'coordinate',
|
||||
'targetPointer', 'targetAuthorPubkey', 'videoEventId',
|
||||
'videoEventRelay', 'videoDefinitionAddress', 'rootIdentifier',
|
||||
'parentCommentId', 'threadParticipantPubkey', 'rootKind',
|
||||
'monthIdentifier', 'encryption', 'hexPubkeys', 'listKey'
|
||||
];
|
||||
|
||||
// Pick some common keys and assign random or interesting values
|
||||
const numKeys = getRandomInt(10);
|
||||
for (let i = 0; i < numKeys; i++) {
|
||||
const key = commonKeys[getRandomInt(commonKeys.length)];
|
||||
const useInteresting = Math.random() < 0.2;
|
||||
input[key] = useInteresting ? interestingValues[getRandomInt(interestingValues.length)] : getRandomValue();
|
||||
}
|
||||
|
||||
// Also add completely random keys
|
||||
const numRandomKeys = getRandomInt(3);
|
||||
for (let i = 0; i < numRandomKeys; i++) {
|
||||
input[getRandomString(5)] = getRandomValue();
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
async function run() {
|
||||
console.log(`Starting fuzzing schemas for ${ITERATIONS} iterations...`);
|
||||
|
||||
const functionsToFuzz = [
|
||||
{ name: 'buildVideoPostEvent', fn: schemas.buildVideoPostEvent },
|
||||
{ name: 'buildVideoMirrorEvent', fn: schemas.buildVideoMirrorEvent },
|
||||
{ name: 'buildRepostEvent', fn: schemas.buildRepostEvent },
|
||||
{ name: 'buildShareEvent', fn: schemas.buildShareEvent },
|
||||
{ name: 'buildRelayListEvent', fn: schemas.buildRelayListEvent },
|
||||
{ name: 'buildDmRelayListEvent', fn: schemas.buildDmRelayListEvent },
|
||||
{ name: 'buildProfileMetadataEvent', fn: schemas.buildProfileMetadataEvent },
|
||||
{ name: 'buildMuteListEvent', fn: schemas.buildMuteListEvent },
|
||||
{ name: 'buildDeletionEvent', fn: schemas.buildDeletionEvent },
|
||||
{ name: 'buildLegacyDirectMessageEvent', fn: schemas.buildLegacyDirectMessageEvent },
|
||||
{ name: 'buildDmAttachmentEvent', fn: schemas.buildDmAttachmentEvent },
|
||||
{ name: 'buildDmReadReceiptEvent', fn: schemas.buildDmReadReceiptEvent },
|
||||
{ name: 'buildDmTypingIndicatorEvent', fn: schemas.buildDmTypingIndicatorEvent },
|
||||
{ name: 'buildViewEvent', fn: schemas.buildViewEvent },
|
||||
{ name: 'buildZapRequestEvent', fn: schemas.buildZapRequestEvent },
|
||||
{ name: 'buildReactionEvent', fn: schemas.buildReactionEvent },
|
||||
{ name: 'buildCommentEvent', fn: schemas.buildCommentEvent },
|
||||
{ name: 'buildWatchHistoryEvent', fn: schemas.buildWatchHistoryEvent },
|
||||
{ name: 'buildSubscriptionListEvent', fn: schemas.buildSubscriptionListEvent },
|
||||
{ name: 'buildBlockListEvent', fn: schemas.buildBlockListEvent },
|
||||
{ name: 'buildHashtagPreferenceEvent', fn: schemas.buildHashtagPreferenceEvent },
|
||||
{ name: 'buildAdminListEvent', fn: (params) => schemas.buildAdminListEvent(params.listKey, params) }, // Wrapper since it takes 2 args
|
||||
];
|
||||
|
||||
const report = {
|
||||
target: 'js/nostrEventSchemas.js',
|
||||
timestamp: new Date().toISOString(),
|
||||
iterations: ITERATIONS,
|
||||
crashes: [],
|
||||
};
|
||||
|
||||
const uniqueCrashes = new Set();
|
||||
|
||||
for (const { name, fn } of functionsToFuzz) {
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
const input = generateFuzzInput();
|
||||
try {
|
||||
const result = fn(input);
|
||||
// Optional: validate output structure
|
||||
// if (result) {
|
||||
// schemas.validateEventAgainstSchema(result.kind, result);
|
||||
// }
|
||||
} catch (err) {
|
||||
const crashKey = `${name}:${err.message}`;
|
||||
if (uniqueCrashes.has(crashKey)) {
|
||||
continue;
|
||||
}
|
||||
uniqueCrashes.add(crashKey);
|
||||
|
||||
let safeInput = "Could not stringify input";
|
||||
try {
|
||||
safeInput = JSON.stringify(input, (k, v) => v === undefined ? '__undefined__' : v);
|
||||
} catch (e) {
|
||||
safeInput = "[Input causes JSON.stringify to throw]";
|
||||
}
|
||||
|
||||
const crashInfo = {
|
||||
function: name,
|
||||
input: safeInput,
|
||||
error: err.toString(),
|
||||
stack: err.stack,
|
||||
};
|
||||
|
||||
// Save reproducer
|
||||
const hash = crypto.createHash('sha256').update(JSON.stringify(crashInfo)).digest('hex').slice(0, 8);
|
||||
const filename = `crash-schemas-${name}-${hash}.json`;
|
||||
fs.writeFileSync(path.join(REPRODUCER_DIR, filename), JSON.stringify(crashInfo, null, 2));
|
||||
|
||||
report.crashes.push({
|
||||
file: filename,
|
||||
function: name,
|
||||
error: err.message
|
||||
});
|
||||
|
||||
console.error(`[CRASH] ${name} failed! Saved to ${filename}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(path.dirname(REPORT_FILE))) {
|
||||
fs.mkdirSync(path.dirname(REPORT_FILE), { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(REPORT_FILE, JSON.stringify(report, null, 2));
|
||||
console.log(`Fuzzing complete. Report saved to ${REPORT_FILE}. Crashes: ${report.crashes.length}`);
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -0,0 +1,55 @@
|
||||
import crypto from 'node:crypto';
|
||||
|
||||
// Mock localStorage
|
||||
const localStorageStore = new Map();
|
||||
const localStorageMock = {
|
||||
getItem: (key) => localStorageStore.get(String(key)) ?? null,
|
||||
setItem: (key, value) => localStorageStore.set(String(key), String(value)),
|
||||
removeItem: (key) => localStorageStore.delete(String(key)),
|
||||
clear: () => localStorageStore.clear(),
|
||||
key: (index) => Array.from(localStorageStore.keys())[index] ?? null,
|
||||
get length() {
|
||||
return localStorageStore.size;
|
||||
},
|
||||
};
|
||||
|
||||
// Mock window
|
||||
const windowMock = {
|
||||
localStorage: localStorageMock,
|
||||
crypto: {
|
||||
getRandomValues: (buffer) => crypto.getRandomValues(buffer),
|
||||
subtle: crypto.webcrypto.subtle,
|
||||
},
|
||||
location: {
|
||||
protocol: 'https:',
|
||||
hostname: 'bitvid.network',
|
||||
href: 'https://bitvid.network/',
|
||||
},
|
||||
};
|
||||
|
||||
// Polyfill globals
|
||||
if (typeof globalThis.window === 'undefined') {
|
||||
globalThis.window = windowMock;
|
||||
}
|
||||
|
||||
if (typeof globalThis.localStorage === 'undefined') {
|
||||
globalThis.localStorage = localStorageMock;
|
||||
}
|
||||
|
||||
if (typeof globalThis.crypto === 'undefined') {
|
||||
globalThis.crypto = windowMock.crypto;
|
||||
}
|
||||
|
||||
if (typeof globalThis.WebSocket === 'undefined') {
|
||||
globalThis.WebSocket = class WebSocket {
|
||||
constructor(url) {
|
||||
this.url = url;
|
||||
this.readyState = 0;
|
||||
}
|
||||
send() {}
|
||||
close() {}
|
||||
};
|
||||
}
|
||||
|
||||
// Mock bitvid-specific globals if needed (though config.js sets them on window)
|
||||
globalThis.bitvidNostrEventOverrides = {};
|
||||
Reference in new issue
Block a user