diff --git a/scripts/agent/fuzz-dmDecryptor.mjs b/scripts/agent/fuzz-dmDecryptor.mjs index ee74ea0a..e3a7aa3b 100644 --- a/scripts/agent/fuzz-dmDecryptor.mjs +++ b/scripts/agent/fuzz-dmDecryptor.mjs @@ -1,236 +1,179 @@ - -import fs from 'node:fs'; -import path from 'node:path'; 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'; -// Setup artifacts and reproducers -const ARTIFACTS_DIR = 'artifacts'; -const REPRODUCERS_DIR = 'examples/reproducers'; -const REPORT_FILE = path.join(ARTIFACTS_DIR, 'fuzz-report-dmDecryptor.json'); +const ITERATIONS = 1000; +const FINDINGS = []; -if (!fs.existsSync(ARTIFACTS_DIR)) fs.mkdirSync(ARTIFACTS_DIR, { recursive: true }); -if (!fs.existsSync(REPRODUCERS_DIR)) fs.mkdirSync(REPRODUCERS_DIR, { recursive: true }); - -const BIG_NAUGHTY_LIST = [ - undefined, - null, - 0, - 1, - -1, - NaN, - Infinity, - -Infinity, - true, - false, - '', - ' ', - '\n', - '\t', - '\r', - '\0', - 'a', - '0', - '[]', - '{}', - 'null', - 'undefined', - 'NaN', - 'Infinity', - '<>', - '\'', - '"', - '`', - '\\', - '/', - '.', - ',', - ':', - ';', - '!', - '?', - '@', - '#', - '$', - '%', - '^', - '&', - '*', - '(', - ')', - '-', - '_', - '+', - '=', - '[', - ']', - '{', - '}', - '|', - '~', - '💩', - 'Hello 🌍', - '\uD800', - '\uDFFF', - '\u0000', - '\uFFFF', - '\uFFFD', - Array(1000).fill('a').join(''), - Object.create(null), - [], - {}, - [null], - [undefined], - [''], - { a: 1 }, -]; - -const recursiveObj = {}; -recursiveObj.self = recursiveObj; -BIG_NAUGHTY_LIST.push(recursiveObj); - -const report = { - target: 'dmDecryptor', - timestamp: new Date().toISOString(), - failures: [], -}; - -function logFailure(funcName, args, error) { - const hash = Math.random().toString(36).substring(2, 10); - const reproDir = path.join(REPRODUCERS_DIR, `issue-${Date.now()}-${hash}`); - fs.mkdirSync(reproDir, { recursive: true }); - - // Simplified serialization for repro script - const serialize = (obj) => { - try { - return JSON.stringify(obj, (key, value) => { - if (value === undefined) return '__UNDEFINED__'; - if (Number.isNaN(value)) return '__NAN__'; - if (value === Infinity) return '__INFINITY__'; - if (typeof value === 'function') return value.toString(); - return value; - }, 2).replace(/"__UNDEFINED__"/g, 'undefined').replace(/"__NAN__"/g, 'NaN').replace(/"__INFINITY__"/g, 'Infinity'); - } catch (e) { - return '{}'; // Fallback +function saveReproducerDM(id, input, error) { + const reproDir = path.join('examples', 'reproducers', `fuzz-dmDecryptor-${id}`); + if (!fs.existsSync(reproDir)) { + fs.mkdirSync(reproDir, { recursive: true }); } - }; - const reproScript = ` -import { ${funcName} } from '../../../js/dmDecryptor.js'; + const readmeContent = `# Fuzz Reproducer: dmDecryptor - ${id} -const args = ${serialize(args)}; - -// Rehydrate functions if any -args.forEach(arg => { - if (arg && arg.decryptors) { - arg.decryptors.forEach(d => { - // This is a rough approximation, real repro might need manual adjustment if functions were complex - // But for fuzzing we usually pass simple mock functions - d.decrypt = async () => "decrypted"; - }); - } -}); - -console.log('Running reproduction for ${funcName}...'); -try { - await ${funcName}(...args); - console.log('No crash reproduced.'); -} catch (error) { - console.log('Crash reproduced:'); - console.error(error); -} +## Error +\`\`\` +${error.stack || error.message} +\`\`\` `; - fs.writeFileSync(path.join(reproDir, 'repro.mjs'), reproScript); - fs.writeFileSync(path.join(reproDir, 'README.md'), `To run:\n\n\`\`\`bash\nnode --import ../../../tests/test-helpers/setup-localstorage.mjs repro.mjs\n\`\`\`\n\nError: ${error.message}\nStack: ${error.stack}`); + fs.writeFileSync(path.join(reproDir, 'README.md'), readmeContent); - report.failures.push({ - function: funcName, - args: args.map(a => { - try { - return JSON.stringify(a); - } catch { - return '[Circular/Unserializable]'; + // Serialize input parts + const event = input.event; + const context = input.context; + + // 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 + }; + + 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 scriptContent = ` +import { decryptDM } from '../../../../js/dmDecryptor.js'; +import fs from 'fs'; +import path from 'path'; + +const event = JSON.parse(fs.readFileSync('input-event.json', 'utf8')); +const contextConfig = JSON.parse(fs.readFileSync('input-context-config.json', 'utf8')); + +// 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"; } - }), - error: { - message: error.message, - stack: error.stack, - }, - reproDir, + })) + : 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}`); +} + +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 }); - console.error(`FAILURE in ${funcName}: ${error.message}`); + saveReproducerDM(id, input, error); } -async function fuzzDecryptDM() { - console.log(`Fuzzing decryptDM...`); - - const mockDecryptor = { - scheme: 'nip44', - decrypt: async (pubkey, ciphertext) => { - if (ciphertext === 'FAIL') throw new Error('Decryption failed intentionally'); - if (ciphertext === 'JUNK') return 'NOT JSON'; - if (ciphertext === 'EMPTY') return ''; - return JSON.stringify({ content: 'secret message', kind: 1, created_at: 12345 }); +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() }; - - const naughtyDecryptors = [ - null, - undefined, - {}, - { decrypt: null }, - { decrypt: 'not a function' }, - { decrypt: async () => { throw new Error('Boom'); } }, - { decrypt: async () => 123 }, // returns non-string - ]; - - // Test 1: Fuzz event argument - for (const input of BIG_NAUGHTY_LIST) { - try { - await decryptDM(input, { decryptors: [mockDecryptor] }); - } catch (error) { - logFailure('decryptDM', [input, { decryptors: ['[MockDecryptor]'] }], error); - } - } - - // Test 2: Fuzz context argument - const validEvent = { kind: 4, content: 'ciphertext', pubkey: '00'.repeat(32), tags: [] }; - for (const input of BIG_NAUGHTY_LIST) { - try { - await decryptDM(validEvent, input); - } catch (error) { - logFailure('decryptDM', [validEvent, input], error); - } - } - - // Test 3: Fuzz decryptors list - for (const input of BIG_NAUGHTY_LIST) { - try { - await decryptDM(validEvent, { decryptors: input }); - } catch (error) { - logFailure('decryptDM', [validEvent, { decryptors: input }], error); - } - } - - // Test 4: Naughty decryptors - for (const badDecryptor of naughtyDecryptors) { - try { - await decryptDM(validEvent, { decryptors: [badDecryptor] }); - } catch (error) { - logFailure('decryptDM', [validEvent, { decryptors: [badDecryptor] }], error); - } - } - - // Test 5: Gift Wrap specific fuzzing (kind 1059) - const giftWrapEvent = { kind: 1059, content: 'ciphertext', pubkey: '00'.repeat(32), tags: [] }; - try { - await decryptDM(giftWrapEvent, { decryptors: [mockDecryptor] }); - } catch (error) { - logFailure('decryptDM', [giftWrapEvent, { decryptors: ['[MockDecryptor]'] }], error); - } } -await fuzzDecryptDM(); +function generateRandomEvent() { + const kind = pickOne([4, 1059, fuzzInt(0, 20000)]); + const content = fuzzBoolean() ? fuzzString(100) : fuzzJSON(2); -fs.writeFileSync(REPORT_FILE, JSON.stringify(report, null, 2)); -console.log(`Fuzzing complete. Report saved to ${REPORT_FILE}`); + 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); +}); diff --git a/scripts/agent/fuzz-magnetUtils.mjs b/scripts/agent/fuzz-magnetUtils.mjs index ffe06e73..09b89736 100644 --- a/scripts/agent/fuzz-magnetUtils.mjs +++ b/scripts/agent/fuzz-magnetUtils.mjs @@ -1,209 +1,90 @@ +import { normalizeAndAugmentMagnet } from '../../js/magnetUtils.js'; +import { + fuzzBoolean, + fuzzInt, + fuzzString, + fuzzHexString, + fuzzJSON, + pickOne, + fuzzSurrogatePairString, + saveFuzzReport, + saveReproducer +} from './fuzz-utils.mjs'; -import fs from 'node:fs'; -import path from 'node:path'; -import { normalizeAndAugmentMagnet, safeDecodeMagnet } from '../../js/magnetUtils.js'; +const ITERATIONS = 1000; +const FINDINGS = []; -// Setup artifacts and reproducers -const ARTIFACTS_DIR = 'artifacts'; -const REPRODUCERS_DIR = 'examples/reproducers'; -const REPORT_FILE = path.join(ARTIFACTS_DIR, 'fuzz-report-magnetUtils.json'); - -if (!fs.existsSync(ARTIFACTS_DIR)) fs.mkdirSync(ARTIFACTS_DIR, { recursive: true }); -if (!fs.existsSync(REPRODUCERS_DIR)) fs.mkdirSync(REPRODUCERS_DIR, { recursive: true }); - -const BIG_NAUGHTY_LIST = [ - undefined, - null, - 0, - 1, - -1, - NaN, - Infinity, - -Infinity, - true, - false, - '', - ' ', - '\n', - '\t', - '\r', - '\0', - 'a', - '0', - '[]', - '{}', - 'null', - 'undefined', - 'NaN', - 'Infinity', - '<>', - '\'', - '"', - '`', - '\\', - '/', - '.', - ',', - ':', - ';', - '!', - '?', - '@', - '#', - '$', - '%', - '^', - '&', - '*', - '(', - ')', - '-', - '_', - '+', - '=', - '[', - ']', - '{', - '}', - '|', - '~', - 'magnet:?', - 'magnet:?xt=urn:btih:', - 'magnet:?xt=urn:btih:123', - 'magnet:?xt=urn:btih:0000000000000000000000000000000000000000', - 'magnet:?tr=udp://tracker.opentrackr.org:1337/announce', - '%', - '%25', - '%%', - 'http://example.com', - 'wss://tracker.example.com', - '\uD800', - '\uDFFF', - '\u0000', - '\uFFFF', - '\uFFFD', - Array(1000).fill('a').join(''), - Object.create(null), - [], - {}, - [null], - [undefined], - [''], - { a: 1 }, -]; - -const recursiveObj = {}; -recursiveObj.self = recursiveObj; -BIG_NAUGHTY_LIST.push(recursiveObj); - -const report = { - target: 'magnetUtils', - timestamp: new Date().toISOString(), - failures: [], -}; - -function logFailure(funcName, args, error) { - const hash = Math.random().toString(36).substring(2, 10); - const reproDir = path.join(REPRODUCERS_DIR, `issue-${Date.now()}-${hash}`); - fs.mkdirSync(reproDir, { recursive: true }); - - const serialize = (obj) => { - try { - return JSON.stringify(obj, (key, value) => { - if (value === undefined) return '__UNDEFINED__'; - if (Number.isNaN(value)) return '__NAN__'; - if (value === Infinity) return '__INFINITY__'; - return value; - }, 2).replace(/"__UNDEFINED__"/g, 'undefined').replace(/"__NAN__"/g, 'NaN').replace(/"__INFINITY__"/g, 'Infinity'); - } catch (e) { - return '{}'; - } - }; - - const reproScript = ` -import { ${funcName} } from '../../../js/magnetUtils.js'; - -const args = ${serialize(args)}; - -console.log('Running reproduction for ${funcName}...'); -try { - ${funcName}(...args); - console.log('No crash reproduced.'); -} catch (error) { - console.log('Crash reproduced:'); - console.error(error); -} -`; - - fs.writeFileSync(path.join(reproDir, 'repro.mjs'), reproScript); - fs.writeFileSync(path.join(reproDir, 'README.md'), `To run:\n\n\`\`\`bash\nnode --import ../../../tests/test-helpers/setup-localstorage.mjs repro.mjs\n\`\`\`\n\nError: ${error.message}\nStack: ${error.stack}`); - - report.failures.push({ - function: funcName, - args: args.map(a => { - try { - return JSON.stringify(a); - } catch { - return '[Circular/Unserializable]'; - } - }), - error: { - message: error.message, - stack: error.stack, - }, - reproDir, +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 }); - console.error(`FAILURE in ${funcName}: ${error.message}`); + saveReproducer(target, id, input, error); } -function fuzzNormalize() { - console.log(`Fuzzing normalizeAndAugmentMagnet...`); +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')}`; - // Test 1: Fuzz rawValue - for (const input of BIG_NAUGHTY_LIST) { - try { - normalizeAndAugmentMagnet(input); - } catch (error) { - logFailure('normalizeAndAugmentMagnet', [input], error); - } + // 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); } + } - // Test 2: Fuzz options - const validMagnet = 'magnet:?xt=urn:btih:0000000000000000000000000000000000000000'; - for (const input of BIG_NAUGHTY_LIST) { - try { - normalizeAndAugmentMagnet(validMagnet, input); - } catch (error) { - logFailure('normalizeAndAugmentMagnet', [validMagnet, input], error); - } - } - - // Test 3: Fuzz specific option fields - const optionKeys = ['webSeed', 'torrentUrl', 'xs', 'extraTrackers', 'appProtocol']; - for (const key of optionKeys) { - for (const input of BIG_NAUGHTY_LIST) { - try { - normalizeAndAugmentMagnet(validMagnet, { [key]: input }); - } catch (error) { - logFailure('normalizeAndAugmentMagnet', [validMagnet, { [key]: input }], error); - } - } + saveFuzzReport('magnetUtils', FINDINGS); + console.log(`Fuzzing complete. Found ${FINDINGS.length} crashes.`); + if (FINDINGS.length > 0) { + process.exit(1); } } -function fuzzSafeDecode() { - console.log(`Fuzzing safeDecodeMagnet...`); - for (const input of BIG_NAUGHTY_LIST) { - try { - safeDecodeMagnet(input); - } catch (error) { - logFailure('safeDecodeMagnet', [input], error); - } - } -} - -fuzzNormalize(); -fuzzSafeDecode(); - -fs.writeFileSync(REPORT_FILE, JSON.stringify(report, null, 2)); -console.log(`Fuzzing complete. Report saved to ${REPORT_FILE}`); +fuzzMagnetUtils(); diff --git a/scripts/agent/fuzz-nostrEventSchemas.mjs b/scripts/agent/fuzz-nostrEventSchemas.mjs index e6046ccf..7f77bf12 100644 --- a/scripts/agent/fuzz-nostrEventSchemas.mjs +++ b/scripts/agent/fuzz-nostrEventSchemas.mjs @@ -1,8 +1,7 @@ - -import fs from 'node:fs'; -import path from 'node:path'; import { buildVideoPostEvent, + buildHttpAuthEvent, + buildReportEvent, buildVideoMirrorEvent, buildRepostEvent, buildShareEvent, @@ -23,226 +22,152 @@ import { buildSubscriptionListEvent, buildBlockListEvent, buildHashtagPreferenceEvent, + buildAdminListEvent, + validateEventStructure, sanitizeAdditionalTags, + NOTE_TYPES } from '../../js/nostrEventSchemas.js'; -// Setup artifacts and reproducers -const ARTIFACTS_DIR = 'artifacts'; -const REPRODUCERS_DIR = 'examples/reproducers'; -const REPORT_FILE = path.join(ARTIFACTS_DIR, 'fuzz-report-nostrEventSchemas.json'); +import { + fuzzBoolean, + fuzzInt, + fuzzString, + fuzzHexString, + fuzzJSON, + pickOne, + fuzzSurrogatePairString, + saveFuzzReport, + saveReproducer +} from './fuzz-utils.mjs'; -if (!fs.existsSync(ARTIFACTS_DIR)) fs.mkdirSync(ARTIFACTS_DIR, { recursive: true }); -if (!fs.existsSync(REPRODUCERS_DIR)) fs.mkdirSync(REPRODUCERS_DIR, { recursive: true }); +const ITERATIONS = 1000; +const FINDINGS = []; -const BIG_NAUGHTY_LIST = [ - undefined, - null, - 0, - 1, - -1, - NaN, - Infinity, - -Infinity, - true, - false, - '', - ' ', - '\n', - '\t', - '\r', - '\0', - 'a', - '0', - '[]', - '{}', - 'null', - 'undefined', - 'NaN', - 'Infinity', - '<>', - '\'', - '"', - '`', - '\\', - '/', - '.', - ',', - ':', - ';', - '!', - '?', - '@', - '#', - '$', - '%', - '^', - '&', - '*', - '(', - ')', - '-', - '_', - '+', - '=', - '[', - ']', - '{', - '}', - '|', - '~', - '💩', // Emoji - 'Hello 🌍', // Text with emoji - 'Zalgo text: Hͥ̽ͣ̃ͤ́eͧ̈́lͥlͦo', // Zalgo - '\uD800', // Lone surrogate - '\uDFFF', - '\uD83D\uDCA9', // Valid surrogate pair (Pile of Poo) - '\u0000', // Null byte - '\uFFFF', - '\uFFFD', // Replacement character - Array(1000).fill('a').join(''), // Long string - { toString: () => { throw new Error('Explosion!'); } }, // Object that throws on toString - { toJSON: () => { throw new Error('JSON Explosion!'); } }, // Object that throws on toJSON - Object.create(null), // Null prototype object - [], - {}, - [null], - [undefined], - [''], - { a: 1 }, +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 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']; + + // 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(); + } + } + }); + + return params; +} + +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); + } + } +} + +console.log('Starting fuzzing for nostrEventSchemas.js...'); + +// 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 a recursive object -const recursiveObj = {}; -recursiveObj.self = recursiveObj; -BIG_NAUGHTY_LIST.push(recursiveObj); +builders.forEach(({ name, fn }) => { + fuzzBuilder(name, fn, () => [generateRandomParams()]); +}); -const report = { - target: 'nostrEventSchemas', - timestamp: new Date().toISOString(), - failures: [], -}; +// Special case for buildAdminListEvent +fuzzBuilder('buildAdminListEvent', buildAdminListEvent, () => { + const listKey = pickOne(['moderation', 'editors', 'whitelist', 'blacklist', fuzzString()]); + return [listKey, generateRandomParams()]; +}); -function logFailure(funcName, args, error) { - const hash = Math.random().toString(36).substring(2, 10); - const reproDir = path.join(REPRODUCERS_DIR, `issue-${Date.now()}-${hash}`); - fs.mkdirSync(reproDir, { recursive: true }); +// Fuzz validateEventStructure +fuzzBuilder('validateEventStructure', validateEventStructure, () => { + const type = pickOne([...Object.values(NOTE_TYPES), fuzzString()]); + const event = fuzzJSON(2); // Random object/JSON + return [type, event]; +}); - const reproScript = ` -import { ${funcName} } from '../../../js/nostrEventSchemas.js'; - -const args = ${JSON.stringify(args, (key, value) => { - if (value === undefined) return '__UNDEFINED__'; - if (Number.isNaN(value)) return '__NAN__'; - if (value === Infinity) return '__INFINITY__'; - if (value === -Infinity) return '__NEG_INFINITY__'; - if (typeof value === 'object' && value !== null) { - // Handle circular references for JSON stringify - const seen = new WeakSet(); - const circularReplacer = (k, v) => { - if (typeof v === 'object' && v !== null) { - if (seen.has(v)) { - return '[Circular]'; - } - seen.add(v); - } - return v; - }; - // Simple circular check handling for top level if needed, but here we just try-catch or simplify - try { - return value; - } catch(e) { - return '[Unserializable]'; - } - } - return value; -}, 2).replace(/"__UNDEFINED__"/g, 'undefined').replace(/"__NAN__"/g, 'NaN').replace(/"__INFINITY__"/g, 'Infinity').replace(/"__NEG_INFINITY__"/g, '-Infinity')}; - -console.log('Running reproduction for ${funcName}...'); -try { - ${funcName}(...args); - console.log('No crash reproduced.'); -} catch (error) { - console.log('Crash reproduced:'); - console.error(error); -} -`; - - fs.writeFileSync(path.join(reproDir, 'repro.mjs'), reproScript); - fs.writeFileSync(path.join(reproDir, 'README.md'), `To run:\n\n\`\`\`bash\nnode --import ../../../tests/test-helpers/setup-localstorage.mjs repro.mjs\n\`\`\`\n\nError: ${error.message}\nStack: ${error.stack}`); - - report.failures.push({ - function: funcName, - args: args.map(a => { - try { - return JSON.stringify(a); - } catch { - return '[Circular/Unserializable]'; +// 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)); } - }), - error: { - message: error.message, - stack: error.stack, - }, - reproDir, - }); - console.error(`FAILURE in ${funcName}: ${error.message}`); -} - -function fuzzFunction(func, funcName) { - console.log(`Fuzzing ${funcName}...`); - - // Test 1: Single argument fuzzing - for (const input of BIG_NAUGHTY_LIST) { - try { - func(input); - } catch (error) { - logFailure(funcName, [input], error); + tags.push(tag); + } else { + tags.push(pickOne([null, undefined, 123, {}, "string"])); + } } + return [tags]; } + // completely random input + return [fuzzJSON(2)]; +}); - // Test 2: Object property fuzzing (since most accept a params object) - const baseParams = {}; - for (const input of BIG_NAUGHTY_LIST) { - // Try to inject the naughty value into common keys - const keys = ['pubkey', 'created_at', 'content', 'tags', 'additionalTags', 'metadata', 'relays']; - for (const key of keys) { - try { - func({ ...baseParams, [key]: input }); - } catch (error) { - logFailure(funcName, [{ [key]: input }], error); - } - } - } +saveFuzzReport('nostrEventSchemas', FINDINGS); +console.log(`Fuzzing complete. Found ${FINDINGS.length} crashes.`); +if (FINDINGS.length > 0) { + process.exit(1); } - -const targets = { - buildVideoPostEvent, - buildVideoMirrorEvent, - buildRepostEvent, - buildShareEvent, - buildRelayListEvent, - buildDmRelayListEvent, - buildProfileMetadataEvent, - buildMuteListEvent, - buildDeletionEvent, - buildLegacyDirectMessageEvent, - buildDmAttachmentEvent, - buildDmReadReceiptEvent, - buildDmTypingIndicatorEvent, - buildViewEvent, - buildZapRequestEvent, - buildReactionEvent, - buildCommentEvent, - buildWatchHistoryEvent, - buildSubscriptionListEvent, - buildBlockListEvent, - buildHashtagPreferenceEvent, - sanitizeAdditionalTags, -}; - -for (const [name, func] of Object.entries(targets)) { - fuzzFunction(func, name); -} - -fs.writeFileSync(REPORT_FILE, JSON.stringify(report, null, 2)); -console.log(`Fuzzing complete. Report saved to ${REPORT_FILE}`); diff --git a/scripts/agent/fuzz-utils.mjs b/scripts/agent/fuzz-utils.mjs index 91e27afe..cf7b25eb 100644 --- a/scripts/agent/fuzz-utils.mjs +++ b/scripts/agent/fuzz-utils.mjs @@ -1,94 +1,127 @@ +import fs from 'fs'; +import path from 'path'; -import { randomBytes } from 'crypto'; - -export function randomString(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; +export function fuzzBoolean() { + return Math.random() < 0.5; } -export function randomUnicodeString(length = 100) { - let result = ''; - for (let i = 0; i < length; i++) { - // Random printable unicode range (roughly) - result += String.fromCharCode(Math.floor(Math.random() * 0xFFFF)); - } - return result; +export function fuzzInt(min = 0, max = 100) { + return Math.floor(Math.random() * (max - min + 1)) + min; } -export function randomSurrogateString(length = 100) { +export function pickOne(options) { + if (!Array.isArray(options) || options.length === 0) return undefined; + return options[Math.floor(Math.random() * options.length)]; +} + +export function fuzzString(length = 10) { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; for (let i = 0; i < length; i++) { - // Generate random surrogates - const code = Math.random() > 0.5 - ? Math.floor(0xD800 + Math.random() * (0xDBFF - 0xD800 + 1)) - : Math.floor(0xDC00 + Math.random() * (0xDFFF - 0xDC00 + 1)); - result += String.fromCharCode(code); + result += chars.charAt(Math.floor(Math.random() * chars.length)); } return result; } -export function randomInt(max = 10000) { - return Math.floor(Math.random() * max); +export function fuzzHexString(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; } -export function randomBool() { - return Math.random() > 0.5; +export function fuzzSurrogatePairString(length = 10) { + // Generate a string that might contain unpaired surrogates + let result = ''; + for (let i = 0; i < length; i++) { + if (Math.random() < 0.1) { + // High surrogate + result += String.fromCharCode(fuzzInt(0xD800, 0xDBFF)); + } else if (Math.random() < 0.1) { + // Low surrogate + result += String.fromCharCode(fuzzInt(0xDC00, 0xDFFF)); + } else { + result += fuzzString(1); + } + } + return result; } -export function randomObject(depth = 2) { - if (depth <= 0) return randomString(10); +export function fuzzJSON(depth = 2) { + if (depth <= 0) { + const type = pickOne(['string', 'number', 'boolean', 'null']); + switch (type) { + case 'string': return fuzzString(); + case 'number': return fuzzInt(); + case 'boolean': return fuzzBoolean(); + case 'null': return null; + } + } + + const type = pickOne(['object', 'array']); + if (type === 'object') { const obj = {}; - const keys = randomInt(5) + 1; + const keys = fuzzInt(1, 5); for (let i = 0; i < keys; i++) { - obj[randomString(5)] = Math.random() > 0.5 ? randomString(10) : randomObject(depth - 1); + obj[fuzzString(5)] = fuzzJSON(depth - 1); } return obj; + } else { + const arr = []; + const len = fuzzInt(1, 5); + for (let i = 0; i < len; i++) { + arr.push(fuzzJSON(depth - 1)); + } + return arr; + } } -export function randomHex(length = 64) { - return randomBytes(length / 2).toString('hex'); +export function saveFuzzReport(targetName, findings) { + const reportDir = 'artifacts'; + if (!fs.existsSync(reportDir)) { + fs.mkdirSync(reportDir, { recursive: true }); + } + const reportPath = path.join(reportDir, `fuzz-report-${targetName}.json`); + const report = { + target: targetName, + date: new Date().toISOString(), + findings: findings + }; + fs.writeFileSync(reportPath, JSON.stringify(report, null, 2)); + console.log(`Fuzz report saved to ${reportPath}`); } -export function pickRandom(arr) { - return arr[Math.floor(Math.random() * arr.length)]; -} +export function saveReproducer(targetName, id, input, error) { + const reproDir = path.join('examples', 'reproducers', `fuzz-${targetName}-${id}`); + if (!fs.existsSync(reproDir)) { + fs.mkdirSync(reproDir, { recursive: true }); + } -export function fuzzGenerators() { - const generators = [ - () => null, - () => undefined, - () => "", - () => " ", - () => 0, - () => 1, - () => -1, - () => NaN, - () => Infinity, - () => -Infinity, - () => [], - () => [null], - () => {}, - () => randomString(1000), - () => randomUnicodeString(100), - () => randomSurrogateString(20), - () => randomObject(), - () => randomHex(64), - () => "magnet:?xt=urn:btih:" + randomHex(40), - () => "https://" + randomString(20) + ".com", - () => { - const obj = {}; - obj.self = obj; - return obj; // Circular - } - ]; - return generators; -} + const readmeContent = `# Fuzz Reproducer: ${targetName} - ${id} -export function getRandomFuzzInput() { - const gens = fuzzGenerators(); - return pickRandom(gens)(); +## Error +\`\`\` +${error.stack || error.message} +\`\`\` + +## Input +See \`input.json\` +`; + + fs.writeFileSync(path.join(reproDir, 'README.md'), readmeContent); + fs.writeFileSync(path.join(reproDir, 'input.json'), JSON.stringify(input, null, 2)); + + // Create a minimal reproduction script + const scriptContent = ` +import fs from 'fs'; +// You would need to adjust imports based on the target +// This is a placeholder template +const input = JSON.parse(fs.readFileSync('input.json', 'utf8')); +console.log('Running reproduction with input:', input); +`; + fs.writeFileSync(path.join(reproDir, 'repro.mjs'), scriptContent); + + console.log(`Reproducer saved to ${reproDir}`); }