Merge pull request #1505 from PR0M3TH3AN/ai/fuzz-nostr-schemas-20260122-10440211805063553313

Fuzz Testing Implementation and Schema Fix
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-22 12:01:07 -05:00
commit c165f41ebe
6 files changed
+560 -439

No files matched your search

@@ -0,0 +1,11 @@
{
"target": "nostrEventSchemas",
"description": "Fixed crash in buildWatchHistoryEvent when pointerTags is null",
"input": {
"action": "buildWatchHistoryEvent",
"args": [{
"pointerTags": null
}]
},
"error": "TypeError: pointerTags.forEach is not a function"
}
+7 -5
View File
@@ -1912,11 +1912,13 @@ export function buildWatchHistoryEvent(params) {
if (monthTagName && identifierValue) {
tags.push([monthTagName, identifierValue]);
}
pointerTags.forEach((tag) => {
if (Array.isArray(tag) && tag.length >= 2) {
tags.push(tag.map((value) => (typeof value === "string" ? value : String(value))));
}
});
if (Array.isArray(pointerTags)) {
pointerTags.forEach((tag) => {
if (Array.isArray(tag) && tag.length >= 2) {
tags.push(tag.map((value) => (typeof value === "string" ? value : String(value))));
}
});
}
appendSchemaTags(tags, schema);
+54 -125
View File
@@ -1,134 +1,63 @@
import { decryptDM } from '../../js/dmDecryptor.js';
import { getRandomFuzzInput, randomHex, randomString } from './fuzz-utils.mjs';
import fs from 'fs';
import path from 'path';
import { decryptDM } from "../../js/dmDecryptor.js";
import { runFuzzer, randomString, randomInt, randomJSON, randomHex, randomBoolean, randomItem } from "./fuzz-shared.mjs";
const REPRODUCERS_DIR = 'examples/reproducers/dmDecryptor';
fs.mkdirSync(REPRODUCERS_DIR, { recursive: true });
async function fuzzDmDecryptor(iteration) {
const schemes = ["nip04", "nip44", "nip44_v2", randomString(5)];
const failures = [];
const mockDecryptor = (scheme) => ({
scheme,
decrypt: async (pubkey, ciphertext, options) => {
// Simulate random failures
if (Math.random() < 0.1) throw new Error("Decryption failed");
function recordFailure(target, input, error) {
const id = Date.now() + Math.random().toString(36).substring(7);
const filename = path.join(REPRODUCERS_DIR, `${target}-${id}.json`);
// Simulate random return values
const type = randomInt(0, 3);
if (type === 0) return randomString(20);
if (type === 1) return JSON.stringify(randomJSON(2, 2)); // Return JSON string
if (type === 2) return ""; // Empty string
if (type === 3) return ciphertext; // Echo
},
source: "mock-fuzzer",
priority: randomInt(0, 10),
supportsGiftWrap: scheme.startsWith("nip44")
});
// Handle circular input for logging
let safeInput = input;
try {
JSON.stringify(input);
} catch {
safeInput = "[Circular or non-serializable input]";
}
const decryptors = [];
const numDecryptors = randomInt(0, 3);
for (let i = 0; i < numDecryptors; i++) {
decryptors.push(mockDecryptor(randomItem(schemes)));
}
const report = {
target,
input: safeInput,
error: {
message: error.message,
stack: error.stack
}
};
const event = {
kind: randomItem([4, 1059, randomInt(0, 20000)]),
pubkey: randomHex(64),
created_at: randomInt(1000000000, 2000000000),
tags: randomJSON(2, 3), // Tags can be anything in fuzzing
content: randomString(100)
};
fs.writeFileSync(filename, JSON.stringify(report, null, 2));
failures.push(report);
console.error(`[FAIL] ${target}: ${error.message} (Report saved to ${filename})`);
// Sometimes provide malformed event
if (Math.random() < 0.05) {
event.kind = "string";
}
if (Math.random() < 0.05) {
event.tags = "string";
}
const context = {
actorPubkey: randomHex(64),
decryptors
};
const input = {
event,
context
};
await decryptDM(event, context);
return input;
}
function runFuzz(name, fn, inputGenerator, iterations = 1000) {
console.log(`Fuzzing ${name} for ${iterations} iterations...`);
for (let i = 0; i < iterations; i++) {
const input = inputGenerator();
try {
fn(input);
} catch (error) {
recordFailure(name, input, error);
}
}
}
// Deterministic Mock decryptor factory based on input seed
const createMockDecryptor = (seedValue) => {
return {
scheme: 'nip44_v2',
decrypt: async (pubkey, ciphertext, options) => {
// Use seed to determine behavior deterministically
// seedValue comes from input.
const mode = (typeof seedValue === 'number' ? seedValue : 0) % 10;
if (mode > 6) {
// Success
return JSON.stringify({ content: 'decrypted', pubkey: randomHex(), created_at: Date.now()/1000 });
} else if (mode > 3) {
// Garbage JSON
return "{ bad json";
} else if (mode > 1) {
// Not JSON
return "Just a string";
} else {
throw new Error('Decrypt failed');
}
}
};
};
const throwingDecryptor = {
scheme: 'nip44',
decrypt: async () => { throw new Error("Hard fail"); }
}
const wrapper = async (input) => {
// input is fuzzy junk.
// We construct a call to decryptDM(event, context)
// Deterministic randomness from input if object
let seed = 0;
if (input && typeof input === 'object') {
seed = Object.keys(input).length;
} else if (typeof input === 'string') {
seed = input.length;
} else if (typeof input === 'number') {
seed = Math.floor(input);
}
let event = input;
// Deterministically force supported kinds based on seed
if (typeof input === 'object' && input !== null && !Array.isArray(input)) {
const kinds = [4, 1059, input.kind];
event = {
...input,
kind: kinds[seed % kinds.length],
content: input.content || 'ciphertext',
pubkey: input.pubkey || randomHex(),
tags: input.tags || [['p', randomHex()]],
created_at: input.created_at || Date.now()/1000
};
}
const mockDec = createMockDecryptor(seed);
let context = {
actorPubkey: randomHex(),
decryptors: [mockDec, throwingDecryptor]
};
// Deterministically fuzz context
if ((seed % 10) > 8) {
context = input.context || getRandomFuzzInput();
}
await decryptDM(event, context);
};
runFuzz('decryptDM', wrapper, getRandomFuzzInput);
// Write summary
fs.writeFileSync('artifacts/fuzz-report-dmDecryptor.json', JSON.stringify(failures, null, 2));
if (failures.length > 0) {
console.log(`\nFuzzing finished with ${failures.length} failures.`);
process.exit(1);
} else {
console.log("\nFuzzing finished with 0 failures.");
process.exit(0);
}
runFuzzer("dmDecryptor", fuzzDmDecryptor, 5000);
+38 -72
View File
@@ -1,81 +1,47 @@
import { normalizeAndAugmentMagnet } from '../../js/magnetUtils.js';
import { getRandomFuzzInput } from './fuzz-utils.mjs';
import fs from 'fs';
import path from 'path';
import { normalizeAndAugmentMagnet } from "../../js/magnetUtils.js";
import { runFuzzer, randomString, randomInt, randomJSON, randomHex, randomBoolean, randomItem } from "./fuzz-shared.mjs";
const REPRODUCERS_DIR = 'examples/reproducers/magnetUtils';
fs.mkdirSync(REPRODUCERS_DIR, { recursive: true });
async function fuzzMagnetUtils(iteration) {
const baseMagnets = [
"magnet:?xt=urn:btih:" + randomHex(40),
"magnet:?xt=urn:btih:" + randomHex(40).toUpperCase(),
"magnet:?xt=urn:btih:" + randomString(40), // Invalid hex
"magnet:?xt=urn:btih:" + randomHex(40) + "&dn=" + randomString(10),
"magnet:?xt=urn:btih:" + randomHex(40) + "&tr=" + encodeURIComponent("wss://" + randomString(10) + ".com"),
randomString(100),
""
];
const failures = [];
const rawValue = Math.random() < 0.8 ? randomItem(baseMagnets) : randomString(100);
function recordFailure(target, input, error) {
const id = Date.now() + Math.random().toString(36).substring(7);
const filename = path.join(REPRODUCERS_DIR, `${target}-${id}.json`);
const options = {
webSeed: Math.random() < 0.5 ? randomString(20) : [randomString(20), randomString(20)],
torrentUrl: Math.random() < 0.5 ? "https://" + randomString(10) + ".com/file.torrent" : randomString(20),
xs: Math.random() < 0.5 ? "https://" + randomString(10) + ".com/file.torrent" : randomString(20),
extraTrackers: Math.random() < 0.5 ? ["wss://" + randomString(10) + ".com"] : randomJSON(1, 2),
appProtocol: randomItem(["http:", "https:", "magnet:", randomString(5)])
};
// Handle circular input for logging
let safeInput = input;
try {
JSON.stringify(input);
} catch {
safeInput = "[Circular or non-serializable input]";
}
// Malformed options
if (Math.random() < 0.1) {
options.webSeed = 123;
}
if (Math.random() < 0.1) {
options.extraTrackers = "string";
}
const report = {
target,
input: safeInput,
error: {
message: error.message,
stack: error.stack
}
};
const input = {
rawValue,
options
};
fs.writeFileSync(filename, JSON.stringify(report, null, 2));
failures.push(report);
console.error(`[FAIL] ${target}: ${error.message} (Report saved to ${filename})`);
// Mock logger to avoid spamming console
options.logger = () => {};
normalizeAndAugmentMagnet(rawValue, options);
return input;
}
function runFuzz(name, fn, inputGenerator, iterations = 1000) {
console.log(`Fuzzing ${name} for ${iterations} iterations...`);
for (let i = 0; i < iterations; i++) {
const input = inputGenerator();
try {
fn(input);
} catch (error) {
recordFailure(name, input, error);
}
}
}
const wrapper = (input) => {
// input is fuzzy junk.
// normalizeAndAugmentMagnet(rawValue, options)
let rawValue = input;
let options = {};
if (typeof input === 'object' && input !== null && !Array.isArray(input)) {
rawValue = input.rawValue || getRandomFuzzInput();
options = input.options || {
webSeed: getRandomFuzzInput(),
torrentUrl: getRandomFuzzInput(),
xs: getRandomFuzzInput(),
extraTrackers: getRandomFuzzInput()
};
}
// Deterministic execution: input fully defines execution path.
normalizeAndAugmentMagnet(rawValue, options);
};
runFuzz('normalizeAndAugmentMagnet', wrapper, getRandomFuzzInput);
// Write summary
fs.writeFileSync('artifacts/fuzz-report-magnetUtils.json', JSON.stringify(failures, null, 2));
if (failures.length > 0) {
console.log(`\nFuzzing finished with ${failures.length} failures.`);
process.exit(1);
} else {
console.log("\nFuzzing finished with 0 failures.");
process.exit(0);
}
runFuzzer("magnetUtils", fuzzMagnetUtils, 10000); // Fast, so more iterations
+313 -237
View File
@@ -1,246 +1,322 @@
import {
buildVideoPostEvent,
buildVideoMirrorEvent,
buildRepostEvent,
buildRelayListEvent,
buildViewEvent,
buildReactionEvent,
buildCommentEvent,
buildWatchHistoryEvent,
buildSubscriptionListEvent,
buildBlockListEvent,
buildHashtagPreferenceEvent,
buildAdminListEvent,
sanitizeAdditionalTags,
getNostrEventSchema,
setNostrEventSchemaOverrides,
NOTE_TYPES
} from '../../js/nostrEventSchemas.js';
import { getRandomFuzzInput, randomString, randomHex } from './fuzz-utils.mjs';
import fs from 'fs';
import path from 'path';
import * as schemas from "../../js/nostrEventSchemas.js";
import { runFuzzer, randomString, randomInt, randomJSON, randomHex, randomBoolean, randomItem } from "./fuzz-shared.mjs";
const REPRODUCERS_DIR = 'examples/reproducers/nostrEventSchemas';
fs.mkdirSync(REPRODUCERS_DIR, { recursive: true });
async function fuzzNostrEventSchemas(iteration) {
// Randomly select an action
const actions = [
"sanitizeAdditionalTags",
"buildVideoPostEvent",
"buildVideoMirrorEvent",
"buildRepostEvent",
"buildRelayListEvent",
"buildDmRelayListEvent",
"buildProfileMetadataEvent",
"buildMuteListEvent",
"buildDeletionEvent",
"buildLegacyDirectMessageEvent",
"buildDmAttachmentEvent",
"buildDmReadReceiptEvent",
"buildDmTypingIndicatorEvent",
"buildViewEvent",
"buildZapRequestEvent",
"buildReactionEvent",
"buildCommentEvent",
"buildWatchHistoryEvent",
"buildSubscriptionListEvent",
"buildBlockListEvent",
"buildHashtagPreferenceEvent",
"buildAdminListEvent"
];
const failures = [];
const action = actions[iteration % actions.length];
// Introduce randomness within the cycle
// const action = randomItem(actions);
function recordFailure(target, input, error) {
const id = Date.now() + Math.random().toString(36).substring(7);
const filename = path.join(REPRODUCERS_DIR, `${target}-${id}.json`);
let input = { action };
// Handle circular input for logging
let safeInput = input;
try {
JSON.stringify(input);
} catch {
safeInput = "[Circular or non-serializable input]";
try {
switch (action) {
case "sanitizeAdditionalTags": {
input.args = [randomJSON(2, 5)];
schemas.sanitizeAdditionalTags(input.args[0]);
break;
}
case "buildVideoPostEvent": {
input.args = [{
pubkey: randomHex(64),
created_at: randomInt(1000000000, 2000000000),
dTagValue: randomString(10),
content: randomJSON(2, 3),
additionalTags: randomJSON(2, 3)
}];
schemas.buildVideoPostEvent(input.args[0]);
break;
}
case "buildVideoMirrorEvent": {
input.args = [{
pubkey: randomHex(64),
created_at: randomInt(1000000000, 2000000000),
tags: randomJSON(2, 3),
content: randomString(100)
}];
schemas.buildVideoMirrorEvent(input.args[0]);
break;
}
case "buildRepostEvent": {
input.args = [{
pubkey: randomHex(64),
created_at: randomInt(1000000000, 2000000000),
eventId: randomHex(64),
eventRelay: randomString(20),
address: randomString(20),
addressRelay: randomString(20),
authorPubkey: randomHex(64),
additionalTags: randomJSON(2, 3),
repostKind: randomInt(0, 10000),
targetKind: randomInt(0, 10000),
targetEvent: randomJSON(2, 3),
serializedEvent: randomString(50)
}];
schemas.buildRepostEvent(input.args[0]);
break;
}
case "buildRelayListEvent": {
input.args = [{
pubkey: randomHex(64),
created_at: randomInt(1000000000, 2000000000),
relays: randomJSON(2, 3),
additionalTags: randomJSON(2, 3)
}];
schemas.buildRelayListEvent(input.args[0]);
break;
}
case "buildDmRelayListEvent": {
input.args = [{
pubkey: randomHex(64),
created_at: randomInt(1000000000, 2000000000),
relays: randomJSON(1, 5),
additionalTags: randomJSON(2, 3)
}];
schemas.buildDmRelayListEvent(input.args[0]);
break;
}
case "buildProfileMetadataEvent": {
input.args = [{
pubkey: randomHex(64),
created_at: randomInt(1000000000, 2000000000),
metadata: randomJSON(2, 3),
content: randomString(50),
additionalTags: randomJSON(2, 3)
}];
schemas.buildProfileMetadataEvent(input.args[0]);
break;
}
case "buildMuteListEvent": {
input.args = [{
pubkey: randomHex(64),
created_at: randomInt(1000000000, 2000000000),
pTags: [randomHex(64), randomHex(64)],
content: randomString(50),
encrypted: randomBoolean(),
encryptionTag: randomString(10),
additionalTags: randomJSON(2, 3)
}];
schemas.buildMuteListEvent(input.args[0]);
break;
}
case "buildDeletionEvent": {
input.args = [{
pubkey: randomHex(64),
created_at: randomInt(1000000000, 2000000000),
eventIds: [randomHex(64)],
addresses: [randomString(20)],
reason: randomString(50),
additionalTags: randomJSON(2, 3)
}];
schemas.buildDeletionEvent(input.args[0]);
break;
}
case "buildLegacyDirectMessageEvent": {
input.args = [{
pubkey: randomHex(64),
created_at: randomInt(1000000000, 2000000000),
recipientPubkey: randomHex(64),
ciphertext: randomString(100),
additionalTags: randomJSON(2, 3)
}];
schemas.buildLegacyDirectMessageEvent(input.args[0]);
break;
}
case "buildDmAttachmentEvent": {
input.args = [{
pubkey: randomHex(64),
created_at: randomInt(1000000000, 2000000000),
recipientPubkey: randomHex(64),
attachment: randomJSON(1, 3),
additionalTags: randomJSON(2, 3)
}];
schemas.buildDmAttachmentEvent(input.args[0]);
break;
}
case "buildDmReadReceiptEvent": {
input.args = [{
pubkey: randomHex(64),
created_at: randomInt(1000000000, 2000000000),
recipientPubkey: randomHex(64),
eventId: randomHex(64),
messageKind: randomInt(0, 10000),
additionalTags: randomJSON(2, 3)
}];
schemas.buildDmReadReceiptEvent(input.args[0]);
break;
}
case "buildDmTypingIndicatorEvent": {
input.args = [{
pubkey: randomHex(64),
created_at: randomInt(1000000000, 2000000000),
recipientPubkey: randomHex(64),
eventId: randomHex(64),
expiresAt: randomInt(1000000000, 2000000000),
additionalTags: randomJSON(2, 3)
}];
schemas.buildDmTypingIndicatorEvent(input.args[0]);
break;
}
case "buildViewEvent": {
// High fuzz target for ensureValidUtf8Content
input.args = [{
pubkey: randomHex(64),
created_at: randomInt(1000000000, 2000000000),
pointerValue: randomString(50),
pointerTag: [randomString(5), randomString(20)],
pointerTags: randomJSON(2, 3),
dedupeTag: randomString(20),
includeSessionTag: randomBoolean(),
additionalTags: randomJSON(2, 3),
content: randomString(200) + "\uD800" + randomString(10) // Surrogate pair
}];
schemas.buildViewEvent(input.args[0]);
break;
}
case "buildZapRequestEvent": {
input.args = [{
pubkey: randomHex(64),
created_at: randomInt(1000000000, 2000000000),
recipientPubkey: randomHex(64),
relays: [randomString(20)],
amountSats: randomInt(0, 1000000),
lnurl: randomString(50),
eventId: randomHex(64),
coordinate: randomString(50),
additionalTags: randomJSON(2, 3),
content: randomString(50)
}];
schemas.buildZapRequestEvent(input.args[0]);
break;
}
case "buildReactionEvent": {
input.args = [{
pubkey: randomHex(64),
created_at: randomInt(1000000000, 2000000000),
pointerValue: randomString(20),
pointerTag: [randomString(5), randomString(20)],
pointerTags: randomJSON(2, 3),
targetPointer: randomJSON(1, 3),
targetAuthorPubkey: randomHex(64),
additionalTags: randomJSON(2, 3),
content: randomString(10)
}];
schemas.buildReactionEvent(input.args[0]);
break;
}
case "buildCommentEvent": {
input.args = [{
pubkey: randomHex(64),
created_at: randomInt(1000000000, 2000000000),
videoEventId: randomHex(64),
videoEventRelay: randomString(20),
videoDefinitionAddress: randomString(50),
videoDefinitionRelay: randomString(20),
rootIdentifier: randomString(50),
rootIdentifierRelay: randomString(20),
parentCommentId: randomHex(64),
parentCommentRelay: randomString(20),
threadParticipantPubkey: randomHex(64),
threadParticipantRelay: randomString(20),
rootKind: randomInt(0, 10000),
rootAuthorPubkey: randomHex(64),
rootAuthorRelay: randomString(20),
parentKind: randomInt(0, 10000),
parentAuthorPubkey: randomHex(64),
parentAuthorRelay: randomString(20),
parentIdentifier: randomString(50),
parentIdentifierRelay: randomString(20),
additionalTags: randomJSON(2, 3),
content: randomString(100)
}];
schemas.buildCommentEvent(input.args[0]);
break;
}
case "buildWatchHistoryEvent": {
input.args = [{
pubkey: randomHex(64),
created_at: randomInt(1000000000, 2000000000),
monthIdentifier: randomString(10),
pointerTags: randomJSON(2, 3),
content: randomJSON(2, 3)
}];
schemas.buildWatchHistoryEvent(input.args[0]);
break;
}
case "buildSubscriptionListEvent": {
input.args = [{
pubkey: randomHex(64),
created_at: randomInt(1000000000, 2000000000),
content: randomString(50),
encryption: randomString(10)
}];
schemas.buildSubscriptionListEvent(input.args[0]);
break;
}
case "buildBlockListEvent": {
input.args = [{
pubkey: randomHex(64),
created_at: randomInt(1000000000, 2000000000),
content: randomString(50),
encryption: randomString(10)
}];
schemas.buildBlockListEvent(input.args[0]);
break;
}
case "buildHashtagPreferenceEvent": {
input.args = [{
pubkey: randomHex(64),
created_at: randomInt(1000000000, 2000000000),
content: randomString(50)
}];
schemas.buildHashtagPreferenceEvent(input.args[0]);
break;
}
case "buildAdminListEvent": {
input.args = [
randomItem(["moderation", "editors", "whitelist", "blacklist", randomString(5)]),
{
pubkey: randomHex(64),
created_at: randomInt(1000000000, 2000000000),
hexPubkeys: [randomHex(64), randomHex(64)]
}
];
schemas.buildAdminListEvent(input.args[0], input.args[1]);
break;
}
}
const report = {
target,
input: safeInput,
error: {
message: error.message,
stack: error.stack
}
};
fs.writeFileSync(filename, JSON.stringify(report, null, 2));
failures.push(report);
console.error(`[FAIL] ${target}: ${error.message} (Report saved to ${filename})`);
return input;
} catch (error) {
throw error;
}
}
function runFuzz(name, fn, inputGenerator, iterations = 1000) {
console.log(`Fuzzing ${name} for ${iterations} iterations...`);
for (let i = 0; i < iterations; i++) {
const input = inputGenerator();
try {
fn(input);
} catch (error) {
recordFailure(name, input, error);
}
}
}
// Wrap functions that take multiple arguments or objects
const wrappers = {
buildVideoPostEvent: (input) => {
if (typeof input === 'object' && input !== null && !Array.isArray(input)) {
buildVideoPostEvent({
pubkey: input.pubkey || getRandomFuzzInput(),
created_at: input.created_at || getRandomFuzzInput(),
dTagValue: input.dTagValue || getRandomFuzzInput(),
content: input.content || getRandomFuzzInput(),
additionalTags: input.additionalTags || getRandomFuzzInput()
});
} else {
buildVideoPostEvent(input);
}
},
buildVideoMirrorEvent: (input) => {
if (typeof input === 'object' && input !== null) {
buildVideoMirrorEvent({
pubkey: input.pubkey || getRandomFuzzInput(),
created_at: input.created_at || getRandomFuzzInput(),
tags: input.tags || getRandomFuzzInput(),
content: input.content || getRandomFuzzInput()
});
} else {
buildVideoMirrorEvent(input);
}
},
buildRepostEvent: (input) => {
if (typeof input === 'object' && input !== null) {
buildRepostEvent({
...input,
pubkey: input.pubkey || getRandomFuzzInput(),
created_at: input.created_at || getRandomFuzzInput()
});
} else {
buildRepostEvent(input);
}
},
buildRelayListEvent: (input) => {
if (typeof input === 'object' && input !== null) {
buildRelayListEvent({
pubkey: input.pubkey || getRandomFuzzInput(),
created_at: input.created_at || getRandomFuzzInput(),
relays: input.relays || getRandomFuzzInput(),
additionalTags: input.additionalTags || getRandomFuzzInput()
});
} else {
buildRelayListEvent(input);
}
},
buildViewEvent: (input) => {
if (typeof input === 'object' && input !== null) {
buildViewEvent({
...input,
pubkey: input.pubkey || getRandomFuzzInput(),
created_at: input.created_at || getRandomFuzzInput()
});
} else {
buildViewEvent(input);
}
},
buildReactionEvent: (input) => {
if (typeof input === 'object' && input !== null) {
buildReactionEvent({
...input,
pubkey: input.pubkey || getRandomFuzzInput(),
created_at: input.created_at || getRandomFuzzInput()
});
} else {
buildReactionEvent(input);
}
},
buildCommentEvent: (input) => {
if (typeof input === 'object' && input !== null) {
buildCommentEvent({
...input,
pubkey: input.pubkey || getRandomFuzzInput(),
created_at: input.created_at || getRandomFuzzInput()
});
} else {
buildCommentEvent(input);
}
},
buildWatchHistoryEvent: (input) => {
if (typeof input === 'object' && input !== null) {
buildWatchHistoryEvent({
...input,
pubkey: input.pubkey || getRandomFuzzInput(),
created_at: input.created_at || getRandomFuzzInput()
});
} else {
buildWatchHistoryEvent(input);
}
},
buildSubscriptionListEvent: (input) => {
if (typeof input === 'object' && input !== null) {
buildSubscriptionListEvent({
...input,
pubkey: input.pubkey || getRandomFuzzInput(),
created_at: input.created_at || getRandomFuzzInput()
});
} else {
buildSubscriptionListEvent(input);
}
},
buildBlockListEvent: (input) => {
if (typeof input === 'object' && input !== null) {
buildBlockListEvent({
...input,
pubkey: input.pubkey || getRandomFuzzInput(),
created_at: input.created_at || getRandomFuzzInput()
});
} else {
buildBlockListEvent(input);
}
},
buildHashtagPreferenceEvent: (input) => {
if (typeof input === 'object' && input !== null) {
buildHashtagPreferenceEvent({
...input,
pubkey: input.pubkey || getRandomFuzzInput(),
created_at: input.created_at || getRandomFuzzInput()
});
} else {
buildHashtagPreferenceEvent(input);
}
},
buildAdminListEvent: (input) => {
// expects listKey and params
const key = getRandomFuzzInput();
if (typeof input === 'object' && input !== null) {
buildAdminListEvent(key, {
...input,
pubkey: input.pubkey || getRandomFuzzInput(),
created_at: input.created_at || getRandomFuzzInput()
});
} else {
buildAdminListEvent(key, input);
}
},
sanitizeAdditionalTags: (input) => {
sanitizeAdditionalTags(input);
},
getNostrEventSchema: (input) => {
getNostrEventSchema(input);
},
setNostrEventSchemaOverrides: (input) => {
try {
setNostrEventSchemaOverrides(input);
// Deterministically pick a type based on input properties if possible
const types = Object.values(NOTE_TYPES);
let index = 0;
if (typeof input === 'object' && input !== null) {
index = Object.keys(input).length % types.length;
} else if (typeof input === 'string') {
index = input.length % types.length;
}
const type = types[index];
getNostrEventSchema(type);
} finally {
// Clean up global state to avoid pollution
setNostrEventSchemaOverrides({});
}
}
};
// Main execution
Object.entries(wrappers).forEach(([name, fn]) => {
runFuzz(name, fn, getRandomFuzzInput);
});
// Write summary
fs.writeFileSync('artifacts/fuzz-report-nostrEventSchemas.json', JSON.stringify(failures, null, 2));
if (failures.length > 0) {
console.log(`\nFuzzing finished with ${failures.length} failures.`);
process.exit(1);
} else {
console.log("\nFuzzing finished with 0 failures.");
process.exit(0);
}
// 5000 iterations to get good coverage
runFuzzer("nostrEventSchemas", fuzzNostrEventSchemas, 5000);
+137
View File
@@ -0,0 +1,137 @@
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const REPRODUCERS_DIR = path.resolve(__dirname, "../../examples/reproducers");
const ARTIFACTS_DIR = path.resolve(__dirname, "../../artifacts");
// Ensure directories exist
if (!fs.existsSync(REPRODUCERS_DIR)) {
fs.mkdirSync(REPRODUCERS_DIR, { recursive: true });
}
if (!fs.existsSync(ARTIFACTS_DIR)) {
fs.mkdirSync(ARTIFACTS_DIR, { recursive: true });
}
export function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
export function randomString(length) {
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 randomHex(length) {
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 randomBoolean() {
return Math.random() < 0.5;
}
export function randomItem(array) {
return array[Math.floor(Math.random() * array.length)];
}
export function randomJSON(depth = 3, maxFields = 5) {
if (depth <= 0) {
const type = randomInt(0, 3);
if (type === 0) return randomString(10);
if (type === 1) return randomInt(-1000, 1000);
if (type === 2) return randomBoolean();
return null;
}
const type = randomInt(0, 5);
if (type === 0) {
// Array
const arr = [];
const len = randomInt(0, maxFields);
for (let i = 0; i < len; i++) {
arr.push(randomJSON(depth - 1, maxFields));
}
return arr;
} else if (type === 1) {
// Object
const obj = {};
const len = randomInt(0, maxFields);
for (let i = 0; i < len; i++) {
obj[randomString(5)] = randomJSON(depth - 1, maxFields);
}
return obj;
} else {
// Scalar
return randomJSON(0);
}
}
export function saveReproducer(target, input, error) {
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const filename = `crash-${target}-${timestamp}.json`;
const filepath = path.join(REPRODUCERS_DIR, filename);
const report = {
target,
timestamp: new Date().toISOString(),
error: {
message: error.message,
stack: error.stack,
name: error.name
},
input
};
fs.writeFileSync(filepath, JSON.stringify(report, null, 2));
console.error(`[FUZZ] Saved reproducer to ${filepath}`);
return filename;
}
export async function runFuzzer(name, fuzzFn, iterations = 1000) {
console.log(`[FUZZ] Starting fuzzer for ${name} with ${iterations} iterations...`);
const crashes = [];
const start = Date.now();
for (let i = 0; i < iterations; i++) {
if (i % 100 === 0) process.stdout.write(".");
let currentInput = null;
try {
const input = await fuzzFn(i);
currentInput = input;
} catch (error) {
process.stdout.write("E");
const filename = saveReproducer(name, currentInput, error);
crashes.push({
iteration: i,
error: error.message,
reproducer: filename
});
}
}
console.log("\n");
const duration = (Date.now() - start) / 1000;
console.log(`[FUZZ] Finished ${name} in ${duration.toFixed(2)}s. Crashes: ${crashes.length}`);
const reportPath = path.join(ARTIFACTS_DIR, `fuzz-report-${name}.json`);
const report = {
target: name,
iterations,
duration,
crashes
};
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
console.log(`[FUZZ] Saved report to ${reportPath}`);
}