Files
bitvid/scripts/agent/fuzz-nostrEventSchemas.mjs
T
google-labs-jules[bot]andPR0M3TH3AN 9f67b9e06b feat: add fuzz testing harnesses and fix crash in nostrEventSchemas
- Expand fuzzing coverage for `js/nostrEventSchemas.js` to include all builder functions.
- Fix a crash in `buildAdminListEvent` where non-array `hexPubkeys` caused a TypeError.
- Update `scripts/agent/fuzz-dmDecryptor.mjs` and `scripts/agent/fuzz-magnetUtils.mjs` iterations.
- Add reproducer for the `buildAdminListEvent` crash.
- Generate fuzz reports in `artifacts/`.

Co-authored-by: PR0M3TH3AN <[email protected]>
2026-01-28 16:27:36 +00:00

255 lines
8.7 KiB
JavaScript

import { runFuzzer, rng } from "./fuzz-shared.mjs";
import { TextEncoder, TextDecoder } from "util";
// Polyfills
global.TextEncoder = TextEncoder;
global.TextDecoder = TextDecoder;
global.window = {
bitvidNostrEventOverrides: {}
};
// Import the target module
// Note: We are in scripts/agent/
import * as Schemas from "../../js/nostrEventSchemas.js";
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.
const targets = [
"buildVideoPostEvent",
"buildViewEvent",
"buildCommentEvent",
"buildReactionEvent",
"buildZapRequestEvent",
"sanitizeAdditionalTags",
"validateEventStructure",
"setNostrEventSchemaOverrides",
"buildRepostEvent",
"buildShareEvent",
"buildRelayListEvent",
"buildDmRelayListEvent",
"buildProfileMetadataEvent",
"buildMuteListEvent",
"buildDeletionEvent",
"buildLegacyDirectMessageEvent",
"buildDmAttachmentEvent",
"buildDmReadReceiptEvent",
"buildDmTypingIndicatorEvent",
"buildWatchHistoryEvent",
"buildSubscriptionListEvent",
"buildBlockListEvent",
"buildHashtagPreferenceEvent",
"buildAdminListEvent",
"buildHttpAuthEvent",
"buildReportEvent",
"buildVideoMirrorEvent"
];
const targetName = rng.oneOf(targets);
let inputs = {};
// Helper to generate a comprehensive random params object
const genParams = () => {
// 10% 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;
// 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(),
};
// 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",
// Added fields
"repostKind", "targetKind", "targetEvent", "serializedEvent",
"video", "metadata", "pTags", "encrypted", "encryptionTag",
"eventIds", "addresses", "reason", "ciphertext", "attachment",
"messageKind", "expiresAt", "monthIdentifier", "hexPubkeys",
"payload", "reportType", "address", "addressRelay", "authorPubkey",
"eventRelay", "method", "userId", "relayHint"
];
extraFields.forEach(field => {
if (rng.bool()) {
params[field] = rng.mixedString(50);
} else if (rng.bool()) {
if (field === "targetEvent" || field === "video" || field === "attachment" || field === "metadata") {
params[field] = rng.recursiveObject(2);
} else if (field === "relays" || field === "pTags" || field === "eventIds" || field === "addresses" || field === "hexPubkeys") {
params[field] = rng.array(() => rng.mixedString(64), 5);
} else {
params[field] = rng.recursiveObject(1); // Potentially invalid type
}
}
});
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;
case "setNostrEventSchemaOverrides":
const randomType = rng.oneOf(Object.values(Schemas.NOTE_TYPES));
if (rng.bool()) {
// Circular object
const obj = { foo: "bar" };
obj.self = obj;
inputs = { [randomType]: obj };
} else {
inputs = { [randomType]: genParams() };
}
// If we set overrides, we should also trigger usage of them to see if it crashes later
Schemas.setNostrEventSchemaOverrides(inputs);
// Trigger usage
Schemas.getNostrEventSchema(randomType);
break;
case "buildRepostEvent":
inputs = genParams();
Schemas.buildRepostEvent(inputs);
break;
case "buildShareEvent":
inputs = genParams();
Schemas.buildShareEvent(inputs);
break;
case "buildRelayListEvent":
inputs = genParams();
Schemas.buildRelayListEvent(inputs);
break;
case "buildDmRelayListEvent":
inputs = genParams();
Schemas.buildDmRelayListEvent(inputs);
break;
case "buildProfileMetadataEvent":
inputs = genParams();
Schemas.buildProfileMetadataEvent(inputs);
break;
case "buildMuteListEvent":
inputs = genParams();
Schemas.buildMuteListEvent(inputs);
break;
case "buildDeletionEvent":
inputs = genParams();
Schemas.buildDeletionEvent(inputs);
break;
case "buildLegacyDirectMessageEvent":
inputs = genParams();
Schemas.buildLegacyDirectMessageEvent(inputs);
break;
case "buildDmAttachmentEvent":
inputs = genParams();
Schemas.buildDmAttachmentEvent(inputs);
break;
case "buildDmReadReceiptEvent":
inputs = genParams();
Schemas.buildDmReadReceiptEvent(inputs);
break;
case "buildDmTypingIndicatorEvent":
inputs = genParams();
Schemas.buildDmTypingIndicatorEvent(inputs);
break;
case "buildWatchHistoryEvent":
inputs = genParams();
Schemas.buildWatchHistoryEvent(inputs);
break;
case "buildSubscriptionListEvent":
inputs = genParams();
Schemas.buildSubscriptionListEvent(inputs);
break;
case "buildBlockListEvent":
inputs = genParams();
Schemas.buildBlockListEvent(inputs);
break;
case "buildHashtagPreferenceEvent":
inputs = genParams();
Schemas.buildHashtagPreferenceEvent(inputs);
break;
case "buildAdminListEvent":
inputs = genParams();
// buildAdminListEvent(listKey, params)
const listKey = rng.oneOf(["moderation", "editors", "whitelist", "blacklist", "communityBlacklistSources", rng.nastyString()]);
Schemas.buildAdminListEvent(listKey, inputs);
break;
case "buildHttpAuthEvent":
inputs = genParams();
Schemas.buildHttpAuthEvent(inputs);
break;
case "buildReportEvent":
inputs = genParams();
Schemas.buildReportEvent(inputs);
break;
case "buildVideoMirrorEvent":
inputs = genParams();
Schemas.buildVideoMirrorEvent(inputs);
break;
}
return { target: targetName, inputs };
}
runFuzzer("nostrEventSchemas", 10000, fuzzTest).catch(err => {
console.error("Fatal fuzzer error:", err);
process.exit(1);
});