mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-30 17:32:26 +00:00
Merge pull request #1505 from PR0M3TH3AN/ai/fuzz-nostr-schemas-20260122-10440211805063553313
Fuzz Testing Implementation and Schema Fix
This commit is contained in:
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"
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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
|
||||
@@ -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);
|
||||
@@ -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}`);
|
||||
}
|
||||
Reference in new issue
Block a user