Merge pull request #1934 from PR0M3TH3AN/ai/fuzz-targets-20250125-13947639872878528331

Add fuzzing harnesses for Nostr schemas, DM decryptor, and magnet utils
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-29 11:55:50 -05:00
commit ac4d27d5cc
7 files changed
+181 -748

No files matched your search

-84
View File
@@ -1,84 +0,0 @@
import "./setup-test-env.js";
import { Fuzzer } from "./fuzz-lib.mjs";
import { decryptDM } from "../../js/dmDecryptor.js";
const fuzzer = new Fuzzer("dm-decryptor");
class MockDecryptor {
constructor(scheme, shouldFail = false) {
this.scheme = scheme;
this.shouldFail = shouldFail;
this.supportsGiftWrap = scheme === "nip44" || scheme === "nip44_v2";
}
async decrypt(pubkey, ciphertext) {
if (this.shouldFail) {
throw new Error("Decryption failed (mock)");
}
// Return something that might be JSON or might be garbage
if (fuzzer.randBool()) {
// Return valid JSON event string (inner rumor)
return JSON.stringify({
kind: 1,
content: "Hello world " + fuzzer.randString(10),
pubkey: fuzzer.randString(64, "0123456789abcdef"),
created_at: Math.floor(Date.now() / 1000),
tags: []
});
} else {
// Return garbage string
return fuzzer.randString(50);
}
}
}
async function fuzzTest(fuzzer, state) {
const schemes = ["nip04", "nip44", "nip44_v2", "unknown"];
// Create decryptors
const decryptors = [];
const numDecryptors = fuzzer.randInt(0, 3);
for(let i=0; i<numDecryptors; i++) {
decryptors.push(new MockDecryptor(fuzzer.pick(schemes), fuzzer.randBool()));
}
// Create event
// kinds: 4 (legacy), 1059 (gift wrap), random
const kinds = [4, 1059, fuzzer.randInt(0, 20000)];
const kind = fuzzer.pick(kinds);
const event = {
kind: kind,
pubkey: fuzzer.randString(64, "0123456789abcdef"),
content: fuzzer.randString(100), // ciphertext
created_at: Math.floor(Date.now() / 1000),
tags: []
};
// Add random tags
const numTags = fuzzer.randInt(0, 5);
for(let i=0; i<numTags; i++) {
event.tags.push(fuzzer.randArray(() => fuzzer.randString(10), 1, 4));
}
// Context
const context = {
actorPubkey: fuzzer.randString(64, "0123456789abcdef"),
decryptors: decryptors
};
state.input = {
event,
context
};
try {
await decryptDM(event, context);
} catch (err) {
// We expect decryptDM to handle errors gracefully and return { ok: false, errors: ... }
// If it throws, it's a bug (except maybe argument validation if we pass null event)
throw err;
}
}
fuzzer.runFuzzLoop(5000, fuzzTest).catch(console.error);
+79 -76
View File
@@ -1,82 +1,85 @@
import "./setup-test-env.js";
import { Fuzzer } from "./fuzz-lib.mjs";
import { decryptDM } from "../../js/dmDecryptor.js";
import fs from "fs";
import path from "path";
import { runFuzzer, rng } from "./fuzz-shared.mjs";
const fuzzer = new Fuzzer("dmDecryptor");
const originalPath = "../../js/dmDecryptor.js";
const tempPath = "./temp_dmDecryptor.mjs";
class MockDecryptor {
constructor(scheme, shouldFail = false) {
this.scheme = scheme;
this.shouldFail = shouldFail;
this.supportsGiftWrap = scheme === "nip44" || scheme === "nip44_v2";
}
// Read original file
const originalContent = fs.readFileSync(new URL(originalPath, import.meta.url), "utf-8");
// Replace import
// The original file has: import { normalizeActorKey } from "./nostr/watchHistory.js";
// We want to point to ./mocks/watchHistory.js
const patchedContent = originalContent.replace(
/import \{ normalizeActorKey \} from "\.\/nostr\/watchHistory\.js";/,
'import { normalizeActorKey } from "./mocks/watchHistory.js";'
);
// Write temp file
fs.writeFileSync(new URL(tempPath, import.meta.url), patchedContent);
// Import the patched module
const DmDecryptor = await import(tempPath);
async function fuzzTest(iteration) {
const genEvent = () => {
const tags = rng.array(() => rng.array(() => rng.mixedString(20), 5), 5);
if (rng.bool()) {
// Circular tag structure
const circularTag = ["p"];
circularTag.push(circularTag); // Circular reference
tags.push(circularTag);
async decrypt(pubkey, ciphertext) {
if (this.shouldFail) {
throw new Error("Decryption failed (mock)");
}
return {
kind: rng.oneOf([4, 1059, rng.int(0, 20000)]),
pubkey: rng.mixedString(64),
created_at: rng.int(0, 2000000000),
content: rng.mixedString(100),
tags
};
};
const genDecryptor = () => {
return {
scheme: rng.oneOf(["nip04", "nip44", "nip44_v2", rng.mixedString(10)]),
decrypt: async (pubkey, ciphertext) => {
if (rng.bool()) {
throw new Error("Decryption failed " + rng.mixedString(10));
}
// Return either a string (valid) or garbage
if (rng.bool()) {
return JSON.stringify({ content: "decrypted message" });
}
return rng.nastyString();
}
};
};
const event = rng.bool() ? genEvent() : rng.nastyString();
const context = {
actorPubkey: rng.mixedString(64),
decryptors: rng.array(genDecryptor, 3)
};
// Run the target
await DmDecryptor.decryptDM(event, context);
return { event, context };
// Return something that might be JSON or might be garbage
if (fuzzer.randBool()) {
// Return valid JSON event string (inner rumor)
return JSON.stringify({
kind: 1,
content: "Hello world " + fuzzer.randString(10),
pubkey: fuzzer.randString(64, "0123456789abcdef"),
created_at: Math.floor(Date.now() / 1000),
tags: []
});
} else {
// Return garbage string
return fuzzer.randString(50);
}
}
}
runFuzzer("dmDecryptor", 10000, fuzzTest)
.catch(err => {
console.error("Fatal fuzzer error:", err);
process.exit(1);
})
.finally(() => {
// Cleanup
if (fs.existsSync(new URL(tempPath, import.meta.url))) {
fs.unlinkSync(new URL(tempPath, import.meta.url));
}
});
async function fuzzTest(fuzzer, state) {
const schemes = ["nip04", "nip44", "nip44_v2", "unknown", null];
// Create decryptors
const decryptors = [];
const numDecryptors = fuzzer.randInt(0, 3);
for(let i=0; i<numDecryptors; i++) {
const scheme = fuzzer.pick(schemes);
decryptors.push(new MockDecryptor(scheme, fuzzer.randBool()));
}
// Create event
// kinds: 4 (legacy), 1059 (gift wrap), random
const kinds = [4, 1059, fuzzer.randInt(0, 20000)];
const kind = fuzzer.pick(kinds);
const event = {
kind: kind,
pubkey: fuzzer.randString(64, "0123456789abcdef"),
content: fuzzer.randString(100), // ciphertext
created_at: Math.floor(Date.now() / 1000),
tags: []
};
// Add random tags
const numTags = fuzzer.randInt(0, 5);
for(let i=0; i<numTags; i++) {
event.tags.push(fuzzer.randArray(() => fuzzer.randString(10), 1, 4));
}
// Context
const context = {
actorPubkey: fuzzer.randString(64, "0123456789abcdef"),
decryptors: decryptors
};
state.input = {
event,
context
};
try {
await decryptDM(event, context);
} catch (err) {
// We expect decryptDM to handle errors gracefully and return { ok: false, errors: ... }
// If it throws, it's a bug (except maybe argument validation if we pass null event)
throw err;
}
}
fuzzer.runFuzzLoop(5000, fuzzTest).catch(console.error);
-38
View File
@@ -1,38 +0,0 @@
import "./setup-test-env.js";
import { Fuzzer } from "./fuzz-lib.mjs";
import { normalizeAndAugmentMagnet } from "../../js/magnetUtils.js";
const fuzzer = new Fuzzer("magnet-utils");
async function fuzzTest(fuzzer, state) {
// Generate inputs
const rawValue = fuzzer.randBool() ? fuzzer.randString(100) : "magnet:?xt=urn:btih:" + fuzzer.randString(40, "0123456789abcdef");
// Inject garbage into magnet link
let malformedValue = rawValue;
if (fuzzer.randBool()) {
// url encode some chars
malformedValue = encodeURIComponent(rawValue);
}
const options = {
webSeed: fuzzer.randBool() ? fuzzer.randString(50) : [fuzzer.randString(50)],
torrentUrl: fuzzer.randString(50),
xs: fuzzer.randString(50),
extraTrackers: fuzzer.randArray(() => fuzzer.randString(30), 0, 3),
appProtocol: fuzzer.randBool() ? "http:" : "https:"
};
state.input = {
rawValue: malformedValue,
options
};
try {
normalizeAndAugmentMagnet(malformedValue, options);
} catch (err) {
throw err;
}
}
fuzzer.runFuzzLoop(5000, fuzzTest).catch(console.error);
+33 -96
View File
@@ -1,107 +1,44 @@
import "./setup-test-env.js";
import { Fuzzer } from "./fuzz-lib.mjs";
import { normalizeAndAugmentMagnet } from "../../js/magnetUtils.js";
import { runFuzzer, rng } from "./fuzz-shared.mjs";
const fuzzer = new Fuzzer("magnetUtils");
// Polyfills
global.window = {
location: {
protocol: "https:"
async function fuzzTest(fuzzer, state) {
// Generate inputs
const rawValue = fuzzer.randBool() ? fuzzer.randString(100) : "magnet:?xt=urn:btih:" + fuzzer.randString(40, "0123456789abcdef");
// Inject garbage into magnet link
let malformedValue = rawValue;
if (fuzzer.randBool()) {
// url encode some chars
malformedValue = encodeURIComponent(rawValue);
}
};
// Import the target module
// Note: We are in scripts/agent/
import * as MagnetUtils from "../../js/magnetUtils.js";
if (fuzzer.randBool()) {
// Inject huge string
malformedValue += fuzzer.randString(10000);
}
async function fuzzTest(iteration) {
// Randomize window protocol sometimes
global.window.location.protocol = rng.bool() ? "https:" : "http:";
const genMagnet = () => {
if (rng.bool()) return rng.mixedString(100); // Random garbage
// Construct semi-valid magnet
let magnet = "magnet:?";
const params = [];
// xt (Exact Topic)
if (rng.bool()) {
const hash = rng.bool() ? rng.mixedString(40) : "urn:btih:" + rng.mixedString(40);
params.push(`xt=${hash}`);
}
// dn (Display Name)
if (rng.bool()) {
const val = rng.mixedString(20);
try {
params.push(`dn=${encodeURIComponent(val)}`);
} catch {
params.push(`dn=${val}`);
}
}
// tr (Trackers)
if (rng.bool()) {
const trackers = rng.array(() => {
const proto = rng.oneOf(["udp", "http", "https", "wss", "ws"]);
const val = `${proto}://${rng.mixedString(20)}`;
try {
return encodeURIComponent(val);
} catch {
return val;
}
}, 5);
trackers.forEach(t => params.push(`tr=${t}`));
}
// ws (Web Seeds)
if (rng.bool()) {
const val = "https://" + rng.mixedString(20);
try {
params.push(`ws=${encodeURIComponent(val)}`);
} catch {
params.push(`ws=${val}`);
}
}
// Random params
if (rng.bool()) {
params.push(`${rng.string(5)}=${rng.mixedString(10)}`);
}
magnet += params.join("&");
// Corrupt it
if (Math.random() < 0.2) {
// Inject garbage
const pos = rng.int(0, magnet.length);
magnet = magnet.slice(0, pos) + rng.nastyString() + magnet.slice(pos);
}
return magnet;
const options = {
webSeed: fuzzer.randBool() ? fuzzer.randString(50) : [fuzzer.randString(50)],
torrentUrl: fuzzer.randString(50),
xs: fuzzer.randString(50),
extraTrackers: fuzzer.randArray(() => fuzzer.randString(30), 0, 3),
appProtocol: fuzzer.randBool() ? "http:" : "https:",
logger: () => {}
};
const rawValue = genMagnet();
const genOptions = () => {
if (rng.bool()) return null;
return {
webSeed: rng.bool() ? rng.mixedString(50) : rng.array(() => rng.mixedString(50), 3),
torrentUrl: rng.mixedString(50),
xs: rng.mixedString(50),
extraTrackers: rng.array(() => rng.mixedString(30), 5),
logger: rng.bool() ? (() => {}) : null,
appProtocol: rng.bool() ? rng.oneOf(["http:", "https:", rng.nastyString()]) : undefined
};
state.input = {
rawValue: malformedValue,
options
};
const options = genOptions();
MagnetUtils.normalizeAndAugmentMagnet(rawValue, options);
return { rawValue, options };
try {
normalizeAndAugmentMagnet(malformedValue, options);
} catch (err) {
throw err;
}
}
runFuzzer("magnetUtils", 10000, fuzzTest).catch(err => {
console.error("Fatal fuzzer error:", err);
process.exit(1);
});
fuzzer.runFuzzLoop(5000, fuzzTest).catch(console.error);
-84
View File
@@ -1,84 +0,0 @@
import "./setup-test-env.js";
import { Fuzzer } from "./fuzz-lib.mjs";
import * as schemas from "../../js/nostrEventSchemas.js";
const fuzzer = new Fuzzer("nostr-schemas");
// Mappers for build functions
const BUILDERS = [
{ name: "buildVideoPostEvent", fn: schemas.buildVideoPostEvent },
{ name: "buildVideoMirrorEvent", fn: schemas.buildVideoMirrorEvent },
{ name: "buildRepostEvent", fn: schemas.buildRepostEvent },
{ name: "buildRelayListEvent", fn: schemas.buildRelayListEvent },
{ name: "buildDmRelayListEvent", fn: schemas.buildDmRelayListEvent },
{ name: "buildProfileMetadataEvent", fn: schemas.buildProfileMetadataEvent },
{ name: "buildMuteListEvent", fn: schemas.buildMuteListEvent },
{ 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 },
];
async function fuzzTest(fuzzer, state) {
// 1. Fuzz Builder Functions
const builder = fuzzer.pick(BUILDERS);
// Generate random params
const params = fuzzer.randJSON();
state.input = {
target: builder.name,
params: params
};
try {
// Some builders require specific args like buildAdminListEvent(listKey, params)
// but most take a single params object.
// We didn't include buildAdminListEvent in the list above because it takes 2 args.
const event = builder.fn(params);
// 2. Validate the generated event
if (event && event.kind) {
// Find schema type for this kind to validate
let type = null;
for (const [key, val] of Object.entries(schemas.NOTE_TYPES)) {
const schema = schemas.getNostrEventSchema(val);
if (schema && schema.kind === event.kind) {
type = val;
break;
}
}
if (type) {
schemas.validateEventAgainstSchema(type, event);
}
}
} catch (err) {
throw err;
}
// 3. Fuzz Validation directly with random events
if (fuzzer.randBool()) {
const types = Object.values(schemas.NOTE_TYPES);
const type = fuzzer.pick(types);
const randomEvent = fuzzer.randJSON();
state.input = {
target: "validateEventAgainstSchema",
type: type,
event: randomEvent
};
schemas.validateEventAgainstSchema(type, randomEvent);
}
}
// Run for 5000 iterations
fuzzer.runFuzzLoop(5000, fuzzTest).catch(console.error);
+69 -243
View File
@@ -1,254 +1,80 @@
import "./setup-test-env.js";
import { Fuzzer } from "./fuzz-lib.mjs";
import * as schemas from "../../js/nostrEventSchemas.js";
import { runFuzzer, rng } from "./fuzz-shared.mjs";
import { TextEncoder, TextDecoder } from "util";
const fuzzer = new Fuzzer("nostrEventSchemas");
// Polyfills
global.TextEncoder = TextEncoder;
global.TextDecoder = TextDecoder;
global.window = {
bitvidNostrEventOverrides: {}
};
// Mappers for build functions
const BUILDERS = Object.keys(schemas)
.filter(k => k.startsWith('build'))
.map(k => ({ name: k, fn: schemas[k] }));
// Import the target module
// Note: We are in scripts/agent/
import * as Schemas from "../../js/nostrEventSchemas.js";
async function fuzzTest(fuzzer, state) {
// 1. Fuzz Builder Functions
const builder = fuzzer.pick(BUILDERS);
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.
// Generate random params
const params = fuzzer.randJSON();
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);
// Inject huge strings occasionally
if (Math.random() < 0.05) {
if (!params || typeof params !== 'object') {
// make it an object
} else {
inputs = rng.nastyString(); // or object, etc.
// add huge string field
params.hugeField = "A".repeat(1000000); // 1MB
}
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 };
state.input = {
target: builder.name,
params: params
};
try {
let args = [params];
if (builder.name === 'buildAdminListEvent') {
const listKeys = ["moderation", "editors", "whitelist", "blacklist", "invalid", null];
args = [fuzzer.pick(listKeys), params];
}
const event = builder.fn(...args);
// 2. Validate the generated event
if (event && event.kind) {
// Find schema type for this kind to validate
let type = null;
for (const [key, val] of Object.entries(schemas.NOTE_TYPES)) {
const schema = schemas.getNostrEventSchema(val);
if (schema && schema.kind === event.kind) {
type = val;
break;
}
}
if (type) {
schemas.validateEventAgainstSchema(type, event);
}
}
} catch (err) {
throw err;
}
// 3. Fuzz Validation directly with random events
if (fuzzer.randBool()) {
const types = Object.values(schemas.NOTE_TYPES);
const type = fuzzer.pick(types);
const randomEvent = fuzzer.randJSON();
state.input = {
target: "validateEventStructure",
type: type,
event: randomEvent
};
schemas.validateEventStructure(type, randomEvent);
}
}
runFuzzer("nostrEventSchemas", 10000, fuzzTest).catch(err => {
console.error("Fatal fuzzer error:", err);
process.exit(1);
});
// Run for 5000 iterations
fuzzer.runFuzzLoop(5000, fuzzTest).catch(console.error);
-127
View File
@@ -1,127 +0,0 @@
import fs from 'fs';
import path from 'path';
export function fuzzBoolean() {
return Math.random() < 0.5;
}
export function fuzzInt(min = 0, max = 100) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
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++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
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 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 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 = fuzzInt(1, 5);
for (let i = 0; i < keys; i++) {
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 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 saveReproducer(targetName, id, input, error) {
const reproDir = path.join('examples', 'reproducers', `fuzz-${targetName}-${id}`);
if (!fs.existsSync(reproDir)) {
fs.mkdirSync(reproDir, { recursive: true });
}
const readmeContent = `# Fuzz Reproducer: ${targetName} - ${id}
## 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}`);
}