mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
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:
7 files changed
+181
-748
No files matched your search
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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}`);
|
||||
}
|
||||
Reference in new issue
Block a user