diff --git a/scripts/agent/fuzz-dm.mjs b/scripts/agent/fuzz-dm.mjs deleted file mode 100644 index 033bfab7..00000000 --- a/scripts/agent/fuzz-dm.mjs +++ /dev/null @@ -1,153 +0,0 @@ -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 0.5 ? 'p' : (Math.random() > 0.5 ? 'encrypted' : getRandomString(5))); - for(let j=1; j 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(); diff --git a/scripts/agent/fuzz-dmDecryptor.mjs b/scripts/agent/fuzz-dmDecryptor.mjs index d236e2a8..8129c88e 100644 --- a/scripts/agent/fuzz-dmDecryptor.mjs +++ b/scripts/agent/fuzz-dmDecryptor.mjs @@ -1,3 +1,4 @@ +// Fuzz harness for js/dmDecryptor.js import "./setup-test-env.js"; import { Fuzzer } from "./fuzz-lib.mjs"; import { decryptDM } from "../../js/dmDecryptor.js"; diff --git a/scripts/agent/fuzz-lib.mjs b/scripts/agent/fuzz-lib.mjs index d09b08cd..96889fda 100644 --- a/scripts/agent/fuzz-lib.mjs +++ b/scripts/agent/fuzz-lib.mjs @@ -1,3 +1,4 @@ +// Fuzzing library for agent scripts import fs from "fs"; import path from "path"; import crypto from "crypto"; diff --git a/scripts/agent/fuzz-magnet.mjs b/scripts/agent/fuzz-magnet.mjs deleted file mode 100644 index 86b8d25a..00000000 --- a/scripts/agent/fuzz-magnet.mjs +++ /dev/null @@ -1,120 +0,0 @@ -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(); diff --git a/scripts/agent/fuzz-magnetUtils.mjs b/scripts/agent/fuzz-magnetUtils.mjs index 4c6add4d..ad33d369 100644 --- a/scripts/agent/fuzz-magnetUtils.mjs +++ b/scripts/agent/fuzz-magnetUtils.mjs @@ -1,3 +1,4 @@ +// Fuzz harness for js/magnetUtils.js import "./setup-test-env.js"; import { Fuzzer } from "./fuzz-lib.mjs"; import { normalizeAndAugmentMagnet } from "../../js/magnetUtils.js"; diff --git a/scripts/agent/fuzz-nostrEventSchemas.mjs b/scripts/agent/fuzz-nostrEventSchemas.mjs index b4fbe321..7b9f3e0a 100644 --- a/scripts/agent/fuzz-nostrEventSchemas.mjs +++ b/scripts/agent/fuzz-nostrEventSchemas.mjs @@ -1,3 +1,4 @@ +// Fuzz harness for js/nostrEventSchemas.js import "./setup-test-env.js"; import { Fuzzer } from "./fuzz-lib.mjs"; import * as schemas from "../../js/nostrEventSchemas.js"; diff --git a/scripts/agent/fuzz-schemas.mjs b/scripts/agent/fuzz-schemas.mjs deleted file mode 100644 index 8a07bc4b..00000000 --- a/scripts/agent/fuzz-schemas.mjs +++ /dev/null @@ -1,217 +0,0 @@ -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 { 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(); diff --git a/scripts/agent/fuzz-setup.mjs b/scripts/agent/fuzz-setup.mjs deleted file mode 100644 index 17cb7525..00000000 --- a/scripts/agent/fuzz-setup.mjs +++ /dev/null @@ -1,55 +0,0 @@ -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 = {}; diff --git a/scripts/agent/fuzz-shared.mjs b/scripts/agent/fuzz-shared.mjs deleted file mode 100644 index 28f6f165..00000000 --- a/scripts/agent/fuzz-shared.mjs +++ /dev/null @@ -1,191 +0,0 @@ - -import fs from "fs"; -import path from "path"; -import { fileURLToPath } from "url"; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const REPRODUCER_DIR = path.join(process.cwd(), "examples/reproducers"); -const ARTIFACTS_DIR = path.join(process.cwd(), "artifacts"); - -// Ensure directories exist -if (!fs.existsSync(REPRODUCER_DIR)) { - fs.mkdirSync(REPRODUCER_DIR, { recursive: true }); -} -if (!fs.existsSync(ARTIFACTS_DIR)) { - fs.mkdirSync(ARTIFACTS_DIR, { recursive: true }); -} - -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)], - - string: (maxLength = 100) => { - const len = Math.floor(Math.random() * maxLength); - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - let result = ""; - for (let i = 0; i < len; i++) { - result += chars.charAt(Math.floor(Math.random() * chars.length)); - } - return result; - }, - - nastyString: () => { - const candidates = [ - "", - "null", - "undefined", - "NaN", - "\x00", - "\n", - "\r", - "\t", - " ", - " ", - "https://example.com", - "wss://relay.example.com", - "", - "' 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++) { - 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 0) { - process.stdout.write("."); - } - } - process.stdout.write("\n"); - - stats.endTime = Date.now(); - stats.duration = stats.endTime - stats.startTime; - - console.log(`[FUZZ] Finished. ${stats.passed} passed, ${stats.failed} failed.`); - saveReport(targetName, stats); - - return stats; -}