mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
feat(agent): add fuzz testing scripts for nostr schemas, DM decryptor, and magnet utils
This commit is contained in:
1 parent
f2344a25d6
commit
a1cb6377a6
5 files changed
+365
-503
No files matched your search
@@ -1,179 +1,75 @@
|
||||
import { decryptDM } from '../../js/dmDecryptor.js';
|
||||
import {
|
||||
fuzzBoolean,
|
||||
fuzzInt,
|
||||
fuzzString,
|
||||
fuzzHexString,
|
||||
fuzzJSON,
|
||||
pickOne,
|
||||
fuzzSurrogatePairString,
|
||||
saveFuzzReport,
|
||||
saveReproducer
|
||||
} from './fuzz-utils.mjs';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const ITERATIONS = 1000;
|
||||
const FINDINGS = [];
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { runFuzzer, rng } from "./fuzz-shared.mjs";
|
||||
|
||||
function saveReproducerDM(id, input, error) {
|
||||
const reproDir = path.join('examples', 'reproducers', `fuzz-dmDecryptor-${id}`);
|
||||
if (!fs.existsSync(reproDir)) {
|
||||
fs.mkdirSync(reproDir, { recursive: true });
|
||||
}
|
||||
const originalPath = "../../js/dmDecryptor.js";
|
||||
const tempPath = "./temp_dmDecryptor.mjs";
|
||||
|
||||
const readmeContent = `# Fuzz Reproducer: dmDecryptor - ${id}
|
||||
// Read original file
|
||||
const originalContent = fs.readFileSync(new URL(originalPath, import.meta.url), "utf-8");
|
||||
|
||||
## Error
|
||||
\`\`\`
|
||||
${error.stack || error.message}
|
||||
\`\`\`
|
||||
`;
|
||||
// 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";'
|
||||
);
|
||||
|
||||
fs.writeFileSync(path.join(reproDir, 'README.md'), readmeContent);
|
||||
// Write temp file
|
||||
fs.writeFileSync(new URL(tempPath, import.meta.url), patchedContent);
|
||||
|
||||
// Serialize input parts
|
||||
const event = input.event;
|
||||
const context = input.context;
|
||||
// Import the patched module
|
||||
const DmDecryptor = await import(tempPath);
|
||||
|
||||
// We can't easily capture the 'shouldFail' state unless we stored it on the object,
|
||||
// but we can infer or just log it. For now, let's just save the structure.
|
||||
|
||||
const serializedContext = {
|
||||
actorPubkey: context?.actorPubkey,
|
||||
decryptorConfigs: context?.decryptors && Array.isArray(context.decryptors) ? context.decryptors.map(d => ({
|
||||
scheme: d.scheme,
|
||||
supportsGiftWrap: d.supportsGiftWrap,
|
||||
})) : context?.decryptors
|
||||
async function fuzzTest(iteration) {
|
||||
const genEvent = () => {
|
||||
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: rng.array(() => rng.array(() => rng.mixedString(20), 5), 5)
|
||||
};
|
||||
};
|
||||
|
||||
fs.writeFileSync(path.join(reproDir, 'input-event.json'), JSON.stringify(event, null, 2));
|
||||
fs.writeFileSync(path.join(reproDir, 'input-context-config.json'), JSON.stringify(serializedContext, null, 2));
|
||||
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 scriptContent = `
|
||||
import { decryptDM } from '../../../../js/dmDecryptor.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
const event = rng.bool() ? genEvent() : rng.nastyString();
|
||||
const context = {
|
||||
actorPubkey: rng.mixedString(64),
|
||||
decryptors: rng.array(genDecryptor, 3)
|
||||
};
|
||||
|
||||
const event = JSON.parse(fs.readFileSync('input-event.json', 'utf8'));
|
||||
const contextConfig = JSON.parse(fs.readFileSync('input-context-config.json', 'utf8'));
|
||||
// Run the target
|
||||
await DmDecryptor.decryptDM(event, context);
|
||||
|
||||
// Reconstruct decryptors
|
||||
const decryptors = Array.isArray(contextConfig.decryptorConfigs)
|
||||
? contextConfig.decryptorConfigs.map(config => ({
|
||||
scheme: config.scheme,
|
||||
supportsGiftWrap: config.supportsGiftWrap,
|
||||
decrypt: async (pubkey, ciphertext) => {
|
||||
console.log('Mock decrypt called for:', pubkey);
|
||||
return "mock-decrypted-content";
|
||||
}
|
||||
}))
|
||||
: contextConfig.decryptorConfigs;
|
||||
|
||||
const context = {
|
||||
actorPubkey: contextConfig.actorPubkey,
|
||||
decryptors
|
||||
};
|
||||
|
||||
console.log('Running reproduction...');
|
||||
try {
|
||||
await decryptDM(event, context);
|
||||
console.log('Execution finished without crash.');
|
||||
} catch (error) {
|
||||
console.error('Crash reproduced:', error);
|
||||
}
|
||||
`;
|
||||
fs.writeFileSync(path.join(reproDir, 'repro.mjs'), scriptContent);
|
||||
console.log(`Reproducer saved to ${reproDir}`);
|
||||
return { event, context };
|
||||
}
|
||||
|
||||
function logError(target, input, error) {
|
||||
const id = Date.now().toString() + Math.floor(Math.random() * 1000);
|
||||
console.error(`[FAIL] ${target} crashed! ID: ${id}`);
|
||||
FINDINGS.push({
|
||||
id,
|
||||
target,
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
input
|
||||
runFuzzer("dmDecryptor", 5000, 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));
|
||||
}
|
||||
});
|
||||
saveReproducerDM(id, input, error);
|
||||
}
|
||||
|
||||
function mockDecryptor(shouldFail = false) {
|
||||
return {
|
||||
scheme: pickOne(['nip04', 'nip44', 'nip44_v2', fuzzString()]),
|
||||
decrypt: async (pubkey, ciphertext) => {
|
||||
if (shouldFail) {
|
||||
throw new Error('Mock Decryption Failed');
|
||||
}
|
||||
// Return a valid stringified JSON event sometimes, or random garbage
|
||||
if (fuzzBoolean()) {
|
||||
return JSON.stringify({
|
||||
content: fuzzString(20),
|
||||
pubkey: fuzzHexString(),
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: []
|
||||
});
|
||||
}
|
||||
return fuzzString(50);
|
||||
},
|
||||
supportsGiftWrap: fuzzBoolean()
|
||||
};
|
||||
}
|
||||
|
||||
function generateRandomEvent() {
|
||||
const kind = pickOne([4, 1059, fuzzInt(0, 20000)]);
|
||||
const content = fuzzBoolean() ? fuzzString(100) : fuzzJSON(2);
|
||||
|
||||
return {
|
||||
kind,
|
||||
pubkey: fuzzHexString(),
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: fuzzBoolean() ? [] : [['p', fuzzHexString()], ['encrypted', 'nip44']],
|
||||
content: typeof content === 'string' ? content : JSON.stringify(content)
|
||||
};
|
||||
}
|
||||
|
||||
function generateRandomContext() {
|
||||
const decryptors = [];
|
||||
const count = fuzzInt(0, 3);
|
||||
for (let i = 0; i < count; i++) {
|
||||
decryptors.push(mockDecryptor(fuzzBoolean())); // some will fail
|
||||
}
|
||||
|
||||
return {
|
||||
actorPubkey: fuzzHexString(),
|
||||
decryptors
|
||||
};
|
||||
}
|
||||
|
||||
async function fuzzDecryptor() {
|
||||
console.log('Starting fuzzing for dmDecryptor.js...');
|
||||
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
const event = generateRandomEvent();
|
||||
const context = generateRandomContext();
|
||||
|
||||
// Also test null/undefined/garbage inputs
|
||||
const inputEvent = pickOne([event, null, undefined, "garbage", {}]);
|
||||
const inputContext = pickOne([context, null, undefined, {}, { decryptors: "garbage" }]);
|
||||
|
||||
try {
|
||||
await decryptDM(inputEvent, inputContext);
|
||||
} catch (error) {
|
||||
logError('decryptDM', { event: inputEvent, context: inputContext }, error);
|
||||
}
|
||||
}
|
||||
|
||||
saveFuzzReport('dmDecryptor', FINDINGS);
|
||||
console.log(`Fuzzing complete. Found ${FINDINGS.length} crashes.`);
|
||||
if (FINDINGS.length > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fuzzDecryptor().catch(err => {
|
||||
console.error('Fuzzing script failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,90 +1,43 @@
|
||||
import { normalizeAndAugmentMagnet } from '../../js/magnetUtils.js';
|
||||
import {
|
||||
fuzzBoolean,
|
||||
fuzzInt,
|
||||
fuzzString,
|
||||
fuzzHexString,
|
||||
fuzzJSON,
|
||||
pickOne,
|
||||
fuzzSurrogatePairString,
|
||||
saveFuzzReport,
|
||||
saveReproducer
|
||||
} from './fuzz-utils.mjs';
|
||||
|
||||
const ITERATIONS = 1000;
|
||||
const FINDINGS = [];
|
||||
import { runFuzzer, rng } from "./fuzz-shared.mjs";
|
||||
|
||||
function logError(target, input, error) {
|
||||
const id = Date.now().toString() + Math.floor(Math.random() * 1000);
|
||||
console.error(`[FAIL] ${target} crashed! ID: ${id}`);
|
||||
FINDINGS.push({
|
||||
id,
|
||||
target,
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
input
|
||||
});
|
||||
saveReproducer(target, id, input, error);
|
||||
}
|
||||
|
||||
function generateRandomMagnet() {
|
||||
const scheme = 'magnet:?';
|
||||
const xt = `xt=urn:btih:${fuzzHexString(40)}`;
|
||||
const dn = `dn=${fuzzString(10)}`;
|
||||
const tr = `tr=${encodeURIComponent('wss://' + fuzzString(10) + '.com')}`;
|
||||
|
||||
// Mix valid parts with garbage
|
||||
const parts = [xt, dn, tr];
|
||||
if (fuzzBoolean()) parts.push(`ws=${encodeURIComponent('https://' + fuzzString(10) + '.com')}`);
|
||||
if (fuzzBoolean()) parts.push(fuzzString(5) + '=' + fuzzString(5));
|
||||
|
||||
// Shuffle
|
||||
parts.sort(() => Math.random() - 0.5);
|
||||
|
||||
return scheme + parts.join('&');
|
||||
}
|
||||
|
||||
function generateRandomOptions() {
|
||||
return {
|
||||
webSeed: fuzzBoolean() ? [fuzzString()] : fuzzString(),
|
||||
torrentUrl: fuzzBoolean() ? fuzzString() : undefined,
|
||||
xs: fuzzBoolean() ? fuzzString() : undefined,
|
||||
extraTrackers: fuzzBoolean() ? [fuzzString()] : [],
|
||||
appProtocol: pickOne(['http:', 'https:', fuzzString(), undefined]),
|
||||
logger: fuzzBoolean() ? () => {} : undefined
|
||||
};
|
||||
}
|
||||
|
||||
function fuzzMagnetUtils() {
|
||||
console.log('Starting fuzzing for magnetUtils.js...');
|
||||
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
// Inputs: valid magnet, partial, garbage, huge string, percent encoded mess
|
||||
const rawValue = pickOne([
|
||||
generateRandomMagnet(),
|
||||
fuzzString(100),
|
||||
`magnet:?xt=${fuzzString(10)}`, // invalid xt
|
||||
fuzzSurrogatePairString(50),
|
||||
null,
|
||||
undefined,
|
||||
{},
|
||||
123
|
||||
]);
|
||||
|
||||
const options = fuzzBoolean() ? generateRandomOptions() : undefined;
|
||||
|
||||
try {
|
||||
normalizeAndAugmentMagnet(rawValue, options);
|
||||
} catch (error) {
|
||||
logError('normalizeAndAugmentMagnet', { rawValue, options }, error);
|
||||
}
|
||||
// Polyfills
|
||||
global.window = {
|
||||
location: {
|
||||
protocol: "https:"
|
||||
}
|
||||
};
|
||||
|
||||
saveFuzzReport('magnetUtils', FINDINGS);
|
||||
console.log(`Fuzzing complete. Found ${FINDINGS.length} crashes.`);
|
||||
if (FINDINGS.length > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
// Import the target module
|
||||
// Note: We are in scripts/agent/
|
||||
import * as MagnetUtils from "../../js/magnetUtils.js";
|
||||
|
||||
async function fuzzTest(iteration) {
|
||||
// Randomize window protocol sometimes
|
||||
global.window.location.protocol = rng.bool() ? "https:" : "http:";
|
||||
|
||||
const rawValue = rng.mixedString(100);
|
||||
|
||||
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
|
||||
};
|
||||
};
|
||||
|
||||
const options = genOptions();
|
||||
|
||||
MagnetUtils.normalizeAndAugmentMagnet(rawValue, options);
|
||||
|
||||
return { rawValue, options };
|
||||
}
|
||||
|
||||
fuzzMagnetUtils();
|
||||
runFuzzer("magnetUtils", 5000, fuzzTest).catch(err => {
|
||||
console.error("Fatal fuzzer error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,173 +1,126 @@
|
||||
import {
|
||||
buildVideoPostEvent,
|
||||
buildHttpAuthEvent,
|
||||
buildReportEvent,
|
||||
buildVideoMirrorEvent,
|
||||
buildRepostEvent,
|
||||
buildShareEvent,
|
||||
buildRelayListEvent,
|
||||
buildDmRelayListEvent,
|
||||
buildProfileMetadataEvent,
|
||||
buildMuteListEvent,
|
||||
buildDeletionEvent,
|
||||
buildLegacyDirectMessageEvent,
|
||||
buildDmAttachmentEvent,
|
||||
buildDmReadReceiptEvent,
|
||||
buildDmTypingIndicatorEvent,
|
||||
buildViewEvent,
|
||||
buildZapRequestEvent,
|
||||
buildReactionEvent,
|
||||
buildCommentEvent,
|
||||
buildWatchHistoryEvent,
|
||||
buildSubscriptionListEvent,
|
||||
buildBlockListEvent,
|
||||
buildHashtagPreferenceEvent,
|
||||
buildAdminListEvent,
|
||||
validateEventStructure,
|
||||
sanitizeAdditionalTags,
|
||||
NOTE_TYPES
|
||||
} from '../../js/nostrEventSchemas.js';
|
||||
|
||||
import {
|
||||
fuzzBoolean,
|
||||
fuzzInt,
|
||||
fuzzString,
|
||||
fuzzHexString,
|
||||
fuzzJSON,
|
||||
pickOne,
|
||||
fuzzSurrogatePairString,
|
||||
saveFuzzReport,
|
||||
saveReproducer
|
||||
} from './fuzz-utils.mjs';
|
||||
import { runFuzzer, rng } from "./fuzz-shared.mjs";
|
||||
import { TextEncoder, TextDecoder } from "util";
|
||||
|
||||
const ITERATIONS = 1000;
|
||||
const FINDINGS = [];
|
||||
// Polyfills
|
||||
global.TextEncoder = TextEncoder;
|
||||
global.TextDecoder = TextDecoder;
|
||||
global.window = {
|
||||
bitvidNostrEventOverrides: {}
|
||||
};
|
||||
|
||||
function logError(target, input, error) {
|
||||
const id = Date.now().toString() + Math.floor(Math.random() * 1000);
|
||||
console.error(`[FAIL] ${target} crashed! ID: ${id}`);
|
||||
FINDINGS.push({
|
||||
id,
|
||||
target,
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
input
|
||||
});
|
||||
saveReproducer(target, id, input, error);
|
||||
}
|
||||
// Import the target module
|
||||
// Note: We are in scripts/agent/
|
||||
import * as Schemas from "../../js/nostrEventSchemas.js";
|
||||
|
||||
function generateRandomParams() {
|
||||
const params = {};
|
||||
const keys = ['pubkey', 'created_at', 'content', 'tags', 'additionalTags', 'dTagValue', 'url', 'method', 'payload', 'eventId', 'userId', 'reportType', 'relayHint', 'recipientPubkey', 'ciphertext', 'attachment', 'messageKind', 'expiresAt', 'pointerValue', 'pointerTag', 'dedupeTag', 'amountSats', 'lnurl', 'coordinate', 'videoEventId', 'rootKind'];
|
||||
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.
|
||||
|
||||
// Randomly select some keys to populate
|
||||
keys.forEach(key => {
|
||||
if (fuzzBoolean()) {
|
||||
if (key === 'created_at' || key === 'amountSats' || key === 'messageKind' || key === 'expiresAt') {
|
||||
params[key] = fuzzBoolean() ? fuzzInt() : fuzzString(); // Mix types
|
||||
} else if (key === 'pubkey' || key === 'recipientPubkey') {
|
||||
params[key] = fuzzBoolean() ? fuzzHexString() : fuzzString();
|
||||
} else if (key === 'tags' || key === 'additionalTags') {
|
||||
params[key] = fuzzBoolean() ? [] : fuzzJSON(2); // Can be valid array or random JSON
|
||||
} else if (key === 'content') {
|
||||
params[key] = fuzzBoolean() ? fuzzSurrogatePairString(100) : fuzzJSON(2);
|
||||
} else {
|
||||
params[key] = fuzzString();
|
||||
}
|
||||
}
|
||||
});
|
||||
const targets = [
|
||||
"buildVideoPostEvent",
|
||||
"buildViewEvent",
|
||||
"buildCommentEvent",
|
||||
"buildReactionEvent",
|
||||
"buildZapRequestEvent",
|
||||
"sanitizeAdditionalTags",
|
||||
"validateEventStructure"
|
||||
];
|
||||
|
||||
return params;
|
||||
}
|
||||
const targetName = rng.oneOf(targets);
|
||||
let inputs = {};
|
||||
|
||||
function fuzzBuilder(name, builderFn, argsGenerator) {
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
const args = argsGenerator();
|
||||
try {
|
||||
if (Array.isArray(args)) {
|
||||
builderFn(...args);
|
||||
} else {
|
||||
builderFn(args);
|
||||
}
|
||||
} catch (error) {
|
||||
logError(name, args, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Helper to generate a comprehensive random params object
|
||||
const genParams = () => {
|
||||
// 50% 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;
|
||||
|
||||
console.log('Starting fuzzing for nostrEventSchemas.js...');
|
||||
// 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(),
|
||||
};
|
||||
|
||||
// Fuzz all builders
|
||||
const builders = [
|
||||
{ name: 'buildVideoPostEvent', fn: buildVideoPostEvent },
|
||||
{ name: 'buildHttpAuthEvent', fn: buildHttpAuthEvent },
|
||||
{ name: 'buildReportEvent', fn: buildReportEvent },
|
||||
{ name: 'buildVideoMirrorEvent', fn: buildVideoMirrorEvent },
|
||||
{ name: 'buildRepostEvent', fn: buildRepostEvent },
|
||||
{ name: 'buildShareEvent', fn: buildShareEvent },
|
||||
{ name: 'buildRelayListEvent', fn: buildRelayListEvent },
|
||||
{ name: 'buildDmRelayListEvent', fn: buildDmRelayListEvent },
|
||||
{ name: 'buildProfileMetadataEvent', fn: buildProfileMetadataEvent },
|
||||
{ name: 'buildMuteListEvent', fn: buildMuteListEvent },
|
||||
{ name: 'buildDeletionEvent', fn: buildDeletionEvent },
|
||||
{ name: 'buildLegacyDirectMessageEvent', fn: buildLegacyDirectMessageEvent },
|
||||
{ name: 'buildDmAttachmentEvent', fn: buildDmAttachmentEvent },
|
||||
{ name: 'buildDmReadReceiptEvent', fn: buildDmReadReceiptEvent },
|
||||
{ name: 'buildDmTypingIndicatorEvent', fn: buildDmTypingIndicatorEvent },
|
||||
{ name: 'buildViewEvent', fn: buildViewEvent },
|
||||
{ name: 'buildZapRequestEvent', fn: buildZapRequestEvent },
|
||||
{ name: 'buildReactionEvent', fn: buildReactionEvent },
|
||||
{ name: 'buildCommentEvent', fn: buildCommentEvent },
|
||||
{ name: 'buildWatchHistoryEvent', fn: buildWatchHistoryEvent },
|
||||
{ name: 'buildSubscriptionListEvent', fn: buildSubscriptionListEvent },
|
||||
{ name: 'buildBlockListEvent', fn: buildBlockListEvent },
|
||||
{ name: 'buildHashtagPreferenceEvent', fn: buildHashtagPreferenceEvent },
|
||||
];
|
||||
// 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"
|
||||
];
|
||||
|
||||
builders.forEach(({ name, fn }) => {
|
||||
fuzzBuilder(name, fn, () => [generateRandomParams()]);
|
||||
});
|
||||
|
||||
// Special case for buildAdminListEvent
|
||||
fuzzBuilder('buildAdminListEvent', buildAdminListEvent, () => {
|
||||
const listKey = pickOne(['moderation', 'editors', 'whitelist', 'blacklist', fuzzString()]);
|
||||
return [listKey, generateRandomParams()];
|
||||
});
|
||||
|
||||
// Fuzz validateEventStructure
|
||||
fuzzBuilder('validateEventStructure', validateEventStructure, () => {
|
||||
const type = pickOne([...Object.values(NOTE_TYPES), fuzzString()]);
|
||||
const event = fuzzJSON(2); // Random object/JSON
|
||||
return [type, event];
|
||||
});
|
||||
|
||||
// Fuzz sanitizeAdditionalTags
|
||||
fuzzBuilder('sanitizeAdditionalTags', sanitizeAdditionalTags, () => {
|
||||
// Random input: array of arrays, array of strings, random object, etc.
|
||||
if (fuzzBoolean()) {
|
||||
// structured but potentially malformed tags
|
||||
const tags = [];
|
||||
const count = fuzzInt(0, 10);
|
||||
for (let i = 0; i < count; i++) {
|
||||
if (fuzzBoolean()) {
|
||||
const tag = [fuzzString()];
|
||||
const args = fuzzInt(0, 5);
|
||||
for (let j = 0; j < args; j++) {
|
||||
tag.push(fuzzBoolean() ? fuzzString() : fuzzJSON(1));
|
||||
extraFields.forEach(field => {
|
||||
if (rng.bool()) {
|
||||
params[field] = rng.mixedString(50);
|
||||
} else if (rng.bool()) {
|
||||
params[field] = rng.recursiveObject(1); // Potentially invalid type
|
||||
}
|
||||
tags.push(tag);
|
||||
} else {
|
||||
tags.push(pickOne([null, undefined, 123, {}, "string"]));
|
||||
}
|
||||
}
|
||||
return [tags];
|
||||
}
|
||||
// completely random input
|
||||
return [fuzzJSON(2)];
|
||||
});
|
||||
});
|
||||
|
||||
saveFuzzReport('nostrEventSchemas', FINDINGS);
|
||||
console.log(`Fuzzing complete. Found ${FINDINGS.length} crashes.`);
|
||||
if (FINDINGS.length > 0) {
|
||||
process.exit(1);
|
||||
return params;
|
||||
};
|
||||
|
||||
switch (targetName) {
|
||||
case "buildVideoPostEvent":
|
||||
inputs = genParams();
|
||||
Schemas.buildVideoPostEvent(inputs);
|
||||
break;
|
||||
|
||||
case "buildViewEvent":
|
||||
inputs = genParams();
|
||||
Schemas.buildViewEvent(inputs);
|
||||
break;
|
||||
|
||||
case "buildCommentEvent":
|
||||
inputs = genParams();
|
||||
Schemas.buildCommentEvent(inputs);
|
||||
break;
|
||||
|
||||
case "buildReactionEvent":
|
||||
inputs = genParams();
|
||||
Schemas.buildReactionEvent(inputs);
|
||||
break;
|
||||
|
||||
case "buildZapRequestEvent":
|
||||
inputs = genParams();
|
||||
Schemas.buildZapRequestEvent(inputs);
|
||||
break;
|
||||
|
||||
case "sanitizeAdditionalTags":
|
||||
// sanitizeAdditionalTags expects an array of tags (arrays)
|
||||
if (rng.bool()) {
|
||||
inputs = rng.array(() => rng.array(() => rng.mixedString(20), 5), 20);
|
||||
} else {
|
||||
inputs = rng.nastyString(); // or object, etc.
|
||||
}
|
||||
Schemas.sanitizeAdditionalTags(inputs);
|
||||
break;
|
||||
|
||||
case "validateEventStructure":
|
||||
const type = rng.oneOf(Object.values(Schemas.NOTE_TYPES));
|
||||
let event = genParams(); // repurpose genParams to make a fake event
|
||||
// Ensure event is an object for this specific test setup, unless we want to fuzz that too.
|
||||
// validateEventStructure checks: if (!event || typeof event !== "object") return { valid: false ... }
|
||||
// So passing a string is valid input for the function (should not crash),
|
||||
// but my fuzzer crashes trying to assign .kind to it.
|
||||
if (event && typeof event === 'object') {
|
||||
event.kind = rng.int(0, 50000);
|
||||
event.tags = rng.array(() => rng.array(() => rng.mixedString(10), 3), 5);
|
||||
}
|
||||
inputs = { type, event };
|
||||
Schemas.validateEventStructure(type, event);
|
||||
break;
|
||||
}
|
||||
|
||||
return { target: targetName, inputs };
|
||||
}
|
||||
|
||||
runFuzzer("nostrEventSchemas", 5000, fuzzTest).catch(err => {
|
||||
console.error("Fatal fuzzer error:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
+149
-95
@@ -5,133 +5,187 @@ 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");
|
||||
|
||||
const REPRODUCER_DIR = path.join(process.cwd(), "examples/reproducers");
|
||||
const ARTIFACTS_DIR = path.join(process.cwd(), "artifacts");
|
||||
|
||||
// Ensure directories exist
|
||||
if (!fs.existsSync(REPRODUCERS_DIR)) {
|
||||
fs.mkdirSync(REPRODUCERS_DIR, { recursive: true });
|
||||
if (!fs.existsSync(REPRODUCER_DIR)) {
|
||||
fs.mkdirSync(REPRODUCER_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 const rng = {
|
||||
bool: () => Math.random() > 0.5,
|
||||
int: (min = 0, max = 100) => Math.floor(Math.random() * (max - min + 1)) + min,
|
||||
float: (min = 0, max = 100) => Math.random() * (max - min) + min,
|
||||
oneOf: (arr) => arr[Math.floor(Math.random() * arr.length)],
|
||||
|
||||
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);
|
||||
string: (maxLength = 100) => {
|
||||
const len = Math.floor(Math.random() * maxLength);
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let result = "";
|
||||
for (let i = 0; i < len; i++) {
|
||||
arr.push(randomJSON(depth - 1, maxFields));
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return arr;
|
||||
} else if (type === 1) {
|
||||
// Object
|
||||
const obj = {};
|
||||
const len = randomInt(0, maxFields);
|
||||
return result;
|
||||
},
|
||||
|
||||
nastyString: () => {
|
||||
const candidates = [
|
||||
"",
|
||||
"null",
|
||||
"undefined",
|
||||
"NaN",
|
||||
"\x00",
|
||||
"\n",
|
||||
"\r",
|
||||
"\t",
|
||||
" ",
|
||||
" ",
|
||||
"https://example.com",
|
||||
"wss://relay.example.com",
|
||||
"<script>alert(1)</script>",
|
||||
"' OR 1=1 --",
|
||||
"javascript:void(0)",
|
||||
"data:text/plain;base64,SGVsbG8=",
|
||||
"😀",
|
||||
"ñ",
|
||||
"\uD83D\uDCA9", // Pile of poo
|
||||
"\uD800", // Lone surrogate (high)
|
||||
"\uDFFF", // Lone surrogate (low)
|
||||
"A".repeat(1000), // Long string
|
||||
"A".repeat(10000), // Very long string
|
||||
JSON.stringify({ foo: "bar" }),
|
||||
"{}",
|
||||
"[]"
|
||||
];
|
||||
return candidates[Math.floor(Math.random() * candidates.length)];
|
||||
},
|
||||
|
||||
mixedString: (maxLength = 100) => {
|
||||
if (Math.random() < 0.3) return rng.nastyString();
|
||||
return rng.string(maxLength);
|
||||
},
|
||||
|
||||
array: (generator, maxLen = 10) => {
|
||||
const len = rng.int(0, maxLen);
|
||||
const result = [];
|
||||
for (let i = 0; i < len; i++) {
|
||||
obj[randomString(5)] = randomJSON(depth - 1, maxFields);
|
||||
result.push(generator());
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
object: (keyGen, valGen, maxKeys = 10) => {
|
||||
const len = rng.int(0, maxKeys);
|
||||
const result = {};
|
||||
for (let i = 0; i < len; i++) {
|
||||
result[keyGen()] = valGen();
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
recursiveObject: (depth = 3) => {
|
||||
if (depth <= 0) return rng.mixedString();
|
||||
|
||||
const type = rng.int(0, 4);
|
||||
if (type === 0) return rng.int(-1000, 1000);
|
||||
if (type === 1) return rng.mixedString();
|
||||
if (type === 2) return rng.bool();
|
||||
if (type === 3) return null;
|
||||
|
||||
if (type === 4) {
|
||||
// Array
|
||||
const len = rng.int(0, 5);
|
||||
const arr = [];
|
||||
for(let i=0; i<len; i++) arr.push(rng.recursiveObject(depth - 1));
|
||||
return arr;
|
||||
}
|
||||
|
||||
// Object
|
||||
const len = rng.int(0, 5);
|
||||
const obj = {};
|
||||
for(let i=0; i<len; i++) {
|
||||
obj[rng.string(10)] = rng.recursiveObject(depth - 1);
|
||||
}
|
||||
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);
|
||||
export function saveReproducer(targetName, input, error) {
|
||||
const timestamp = Date.now();
|
||||
const filename = `fuzz-${targetName}-${timestamp}.json`;
|
||||
const filepath = path.join(REPRODUCER_DIR, filename);
|
||||
|
||||
const report = {
|
||||
target,
|
||||
const content = {
|
||||
target: targetName,
|
||||
timestamp: new Date().toISOString(),
|
||||
error: {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
name: error.name
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
code: error.code,
|
||||
name: error.name
|
||||
},
|
||||
input
|
||||
};
|
||||
|
||||
fs.writeFileSync(filepath, JSON.stringify(report, null, 2));
|
||||
fs.writeFileSync(filepath, JSON.stringify(content, null, 2));
|
||||
console.error(`[FUZZ] Saved reproducer to ${filepath}`);
|
||||
return filename;
|
||||
return filepath;
|
||||
}
|
||||
|
||||
export async function runFuzzer(name, fuzzFn, iterations = 1000) {
|
||||
console.log(`[FUZZ] Starting fuzzer for ${name} with ${iterations} iterations...`);
|
||||
const crashes = [];
|
||||
const start = Date.now();
|
||||
export function saveReport(targetName, stats) {
|
||||
const filepath = path.join(ARTIFACTS_DIR, `fuzz-report-${targetName}.json`);
|
||||
fs.writeFileSync(filepath, JSON.stringify(stats, null, 2));
|
||||
console.log(`[FUZZ] Saved report to ${filepath}`);
|
||||
}
|
||||
|
||||
export async function runFuzzer(targetName, iterations, testFn) {
|
||||
console.log(`[FUZZ] Starting fuzzer for ${targetName} with ${iterations} iterations...`);
|
||||
|
||||
const stats = {
|
||||
target: targetName,
|
||||
iterations: 0,
|
||||
passed: 0,
|
||||
failed: 0,
|
||||
crashes: [], // List of { input, error, reproducerPath }
|
||||
startTime: Date.now(),
|
||||
endTime: null
|
||||
};
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
if (i % 100 === 0) process.stdout.write(".");
|
||||
let currentInput = null;
|
||||
stats.iterations++;
|
||||
let input = null;
|
||||
|
||||
try {
|
||||
const input = await fuzzFn(i);
|
||||
currentInput = input;
|
||||
} catch (error) {
|
||||
process.stdout.write("E");
|
||||
const filename = saveReproducer(name, currentInput, error);
|
||||
crashes.push({
|
||||
input = await testFn(i);
|
||||
stats.passed++;
|
||||
} catch (err) {
|
||||
stats.failed++;
|
||||
const reproducerPath = saveReproducer(targetName, input, err);
|
||||
stats.crashes.push({
|
||||
iteration: i,
|
||||
error: error.message,
|
||||
reproducer: filename
|
||||
error: err.message,
|
||||
reproducerPath
|
||||
});
|
||||
// Optionally stop on first crash or continue
|
||||
// For now, we continue but log it.
|
||||
}
|
||||
|
||||
if (i % 100 === 0 && i > 0) {
|
||||
process.stdout.write(".");
|
||||
}
|
||||
}
|
||||
process.stdout.write("\n");
|
||||
|
||||
console.log("\n");
|
||||
const duration = (Date.now() - start) / 1000;
|
||||
console.log(`[FUZZ] Finished ${name} in ${duration.toFixed(2)}s. Crashes: ${crashes.length}`);
|
||||
stats.endTime = Date.now();
|
||||
stats.duration = stats.endTime - stats.startTime;
|
||||
|
||||
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}`);
|
||||
console.log(`[FUZZ] Finished. ${stats.passed} passed, ${stats.failed} failed.`);
|
||||
saveReport(targetName, stats);
|
||||
|
||||
return stats;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
export function normalizeActorKey(key) {
|
||||
if (typeof key !== 'string') return '';
|
||||
// simple hex check or return as is for mocking
|
||||
return key.toLowerCase().trim();
|
||||
}
|
||||
Reference in new issue
Block a user