Files
bitvid/scripts/agent/fuzz-nostr-schemas.mjs

280 lines
7.2 KiB
JavaScript

import {
buildVideoPostEvent,
buildHttpAuthEvent,
buildReportEvent,
buildGiftWrapEvent,
buildSealEvent,
buildChatMessageEvent,
buildVideoMirrorEvent,
buildRepostEvent,
buildShareEvent,
buildRelayListEvent,
buildDmRelayListEvent,
buildProfileMetadataEvent,
buildMuteListEvent,
buildDeletionEvent,
buildLegacyDirectMessageEvent,
buildDmAttachmentEvent,
buildDmReadReceiptEvent,
buildDmTypingIndicatorEvent,
buildViewEvent,
buildZapRequestEvent,
buildReactionEvent,
buildCommentEvent,
buildWatchHistoryEvent,
buildSubscriptionListEvent,
buildBlockListEvent,
buildHashtagPreferenceEvent,
validateEventStructure,
NOTE_TYPES
} from '../../js/nostrEventSchemas.js';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const REPRO_DIR = path.join(__dirname, '../../examples/reproducers');
const ARTIFACTS_DIR = path.join(__dirname, '../../artifacts');
// Ensure directories exist
if (!fs.existsSync(REPRO_DIR)) fs.mkdirSync(REPRO_DIR, { recursive: true });
if (!fs.existsSync(ARTIFACTS_DIR)) fs.mkdirSync(ARTIFACTS_DIR, { recursive: true });
const TARGETS = {
buildVideoPostEvent,
buildHttpAuthEvent,
buildReportEvent,
buildGiftWrapEvent,
buildSealEvent,
buildChatMessageEvent,
buildVideoMirrorEvent,
buildRepostEvent,
buildShareEvent,
buildRelayListEvent,
buildDmRelayListEvent,
buildProfileMetadataEvent,
buildMuteListEvent,
buildDeletionEvent,
buildLegacyDirectMessageEvent,
buildDmAttachmentEvent,
buildDmReadReceiptEvent,
buildDmTypingIndicatorEvent,
buildViewEvent,
buildZapRequestEvent,
buildReactionEvent,
buildCommentEvent,
buildWatchHistoryEvent,
buildSubscriptionListEvent,
buildBlockListEvent,
buildHashtagPreferenceEvent,
};
// Fuzzing Configuration
const CONFIG = {
iterations: 2000,
seed: process.env.SEED || Date.now().toString(),
maxDepth: 3,
maxStringLength: 1024,
timeoutMs: 5000 // Per function call
};
console.log(`[Fuzz] Starting fuzz run with seed: ${CONFIG.seed}`);
// Random Generator
class Random {
constructor(seed) {
this.state = BigInt(seed);
}
next() {
// Simple LCG
this.state = (this.state * 6364136223846793005n + 1442695040888963407n) % 18446744073709551616n;
return Number(this.state) / Number(18446744073709551616n);
}
bool() { return this.next() > 0.5; }
int(min, max) {
return Math.floor(this.next() * (max - min + 1)) + min;
}
pick(arr) {
if (arr.length === 0) return undefined;
return arr[this.int(0, arr.length - 1)];
}
string(length = 10) {
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+{}|:"<>?';
let res = '';
const len = this.int(0, length);
for (let i = 0; i < len; i++) res += this.pick(chars);
return res;
}
unicodeString(length = 10) {
let res = '';
const len = this.int(0, length);
for (let i = 0; i < len; i++) res += String.fromCodePoint(this.int(0, 0x10FFFF));
return res;
}
}
const rng = new Random(CONFIG.seed);
// Input Generators
const GENERATORS = {
null: () => null,
undefined: () => undefined,
number: () => rng.int(-10000, 10000),
float: () => rng.next() * 10000,
boolean: () => rng.bool(),
string: () => rng.string(rng.int(0, 100)),
longString: () => rng.string(rng.int(1000, CONFIG.maxStringLength)),
unicodeString: () => rng.unicodeString(rng.int(0, 100)),
emptyArray: () => [],
emptyObject: () => ({}),
// Specific nasty inputs
surrogatePair: () => '\uD800', // Unmatched surrogate
constructorKey: () => 'constructor',
prototypeKey: () => '__proto__',
// Composite generators
array: (depth = 0) => {
if (depth > CONFIG.maxDepth) return [];
const len = rng.int(0, 5);
const arr = [];
for (let i = 0; i < len; i++) {
arr.push(generateAny(depth + 1));
}
return arr;
},
object: (depth = 0) => {
if (depth > CONFIG.maxDepth) return {};
const len = rng.int(0, 5);
const obj = {};
for (let i = 0; i < len; i++) {
obj[rng.string(5)] = generateAny(depth + 1);
}
return obj;
}
};
function generateAny(depth = 0) {
const types = Object.keys(GENERATORS).filter(k => k !== 'array' && k !== 'object');
// 20% chance of composite types if depth allows
if (depth < CONFIG.maxDepth && rng.bool()) {
types.push('array', 'object');
}
const type = rng.pick(types);
return GENERATORS[type](depth);
}
// Generate specifically malicious params for builder functions
function generateParams() {
// Sometimes return non-object
if (rng.next() < 0.1) return generateAny();
const params = {};
const keys = [
'pubkey', 'created_at', 'content', 'additionalTags', 'tags', 'relays',
'id', 'eventId', 'recipientPubkey', 'url', 'magnet', 'title', 'dTagValue',
'metadata', 'reason', 'attachment', 'pTags'
];
// Fuzz common keys
keys.forEach(key => {
if (rng.bool()) params[key] = generateAny();
});
// Inject specific structure for additionalTags (common crash point: expecting array of arrays)
if (rng.bool()) {
params.additionalTags = rng.next() < 0.5
? generateAny() // Random junk
: [generateAny(), generateAny()]; // Array of junk
}
return params;
}
// Report
const report = {
target: 'nostrEventSchemas.js',
seed: CONFIG.seed,
iterations: CONFIG.iterations,
startTime: new Date().toISOString(),
failures: []
};
// Runner
function runFuzz() {
let passed = 0;
let failed = 0;
for (const [funcName, func] of Object.entries(TARGETS)) {
console.log(`Fuzzing ${funcName}...`);
for (let i = 0; i < CONFIG.iterations; i++) {
const input = generateParams();
try {
func(input);
passed++;
} catch (err) {
failed++;
console.error(`[FAILURE] ${funcName} crashed!`);
console.error(err);
const caseId = `${funcName}-${Date.now()}-${i}`;
const reproPath = path.join(REPRO_DIR, `fuzz-nostr-schemas-${new Date().toISOString().split('T')[0]}`, caseId);
fs.mkdirSync(reproPath, { recursive: true });
// Save Input
fs.writeFileSync(path.join(reproPath, 'input.json'), JSON.stringify(input, null, 2));
// Save Repro Script
const reproScript = `
import { ${funcName} } from '../../../../../js/nostrEventSchemas.js';
const input = ${JSON.stringify(input, null, 2)};
try {
${funcName}(input);
console.log('Did not crash.');
} catch (e) {
console.error('Crashed:', e);
process.exit(1);
}
`;
fs.writeFileSync(path.join(reproPath, 'repro.mjs'), reproScript);
report.failures.push({
caseId,
function: funcName,
error: err.message,
stack: err.stack,
reproPath
});
}
}
}
report.endTime = new Date().toISOString();
report.summary = { passed, failed };
// Write report
const reportPath = path.join(ARTIFACTS_DIR, `fuzz-report-nostr-schemas-${new Date().toISOString().split('T')[0]}.json`);
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log(`[Fuzz] Finished. Passed: ${passed}, Failed: ${failed}`);
console.log(`[Fuzz] Report saved to ${reportPath}`);
if (failed > 0) {
process.exit(1);
}
}
runFuzz();