mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
feat(fuzz): add fuzzing suite for schemas, dms, and magnets
- Add `scripts/agent/fuzz-nostr-schemas.mjs` to fuzz event builders and validation - Add `scripts/agent/fuzz-dm-decryptor.mjs` to fuzz DM unwrapping - Add `scripts/agent/fuzz-magnet-utils.mjs` to fuzz magnet normalization - Utilize `scripts/agent/fuzz-lib.mjs` for randomization and reporting - Verify that `scripts/agent/setup-test-env.js` is imported to polyfill globals - Output reports to `artifacts/` No crashes were found during the initial fuzzing run (5000 iterations per target).
This commit is contained in:
1 parent
a99a9f3231
commit
3e38916f35
3 files changed
+172
-180
No files matched your search
@@ -1,72 +1,84 @@
|
||||
import "./setup-test-env.js";
|
||||
import { Fuzzer } from "./fuzz-lib.mjs";
|
||||
import { decryptDM } from "../../js/dmDecryptor.js";
|
||||
|
||||
// Mock global dependencies
|
||||
if (typeof global.window === "undefined") {
|
||||
global.window = {
|
||||
location: { protocol: "https:" },
|
||||
crypto: {
|
||||
getRandomValues: (arr) => crypto.randomFillSync(arr),
|
||||
subtle: {
|
||||
digest: async (algo, data) => {
|
||||
const hash = crypto.createHash("sha256").update(data).digest();
|
||||
return hash.buffer;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
if (typeof global.localStorage === "undefined") {
|
||||
global.localStorage = {
|
||||
getItem: () => null,
|
||||
setItem: () => {},
|
||||
removeItem: () => {}
|
||||
};
|
||||
}
|
||||
|
||||
const fuzzer = new Fuzzer("dm-decryptor");
|
||||
|
||||
async function test(fuzzer, state) {
|
||||
// Generate random event
|
||||
const event = {
|
||||
kind: fuzzer.randBool() ? fuzzer.pick([4, 1059]) : fuzzer.randInt(0, 20000), // 4=DM, 1059=GiftWrap
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: fuzzer.randArray(() => {
|
||||
const tagType = fuzzer.pick(["p", "e", "encrypted"]);
|
||||
if (tagType === "p") return ["p", fuzzer.randString(64, "0123456789abcdef")];
|
||||
if (tagType === "encrypted") return ["encrypted", fuzzer.pick(["nip04", "nip44", "nip44_v2"])];
|
||||
return [fuzzer.randString(5), fuzzer.randString(10)];
|
||||
}, 0, 5),
|
||||
content: fuzzer.randUnicodeString(fuzzer.randInt(0, 200)),
|
||||
pubkey: fuzzer.randString(64, "0123456789abcdef"),
|
||||
id: fuzzer.randString(64, "0123456789abcdef")
|
||||
};
|
||||
class MockDecryptor {
|
||||
constructor(scheme, shouldFail = false) {
|
||||
this.scheme = scheme;
|
||||
this.shouldFail = shouldFail;
|
||||
this.supportsGiftWrap = scheme === "nip44" || scheme === "nip44_v2";
|
||||
}
|
||||
|
||||
if (fuzzer.randBool()) event.content = fuzzer.randJSON(); // Garbage content
|
||||
|
||||
// Mock decryptors
|
||||
const decryptors = fuzzer.randArray(() => {
|
||||
return {
|
||||
decrypt: async (pubkey, ciphertext) => {
|
||||
if (fuzzer.randBool()) throw new Error("Decrypt failed");
|
||||
// Return random valid JSON or garbage
|
||||
if (fuzzer.randBool()) return JSON.stringify(fuzzer.randJSON());
|
||||
return fuzzer.randUnicodeString(50);
|
||||
},
|
||||
scheme: fuzzer.pick(["nip04", "nip44", "nip44_v2", "unknown"]),
|
||||
priority: fuzzer.randInt(0, 10),
|
||||
supportsGiftWrap: fuzzer.randBool()
|
||||
};
|
||||
}, 0, 3);
|
||||
|
||||
const context = {
|
||||
actorPubkey: fuzzer.randString(64, "0123456789abcdef"),
|
||||
decryptors: decryptors
|
||||
};
|
||||
|
||||
state.input = { event, context };
|
||||
|
||||
await decryptDM(event, context);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fuzzer.runFuzzLoop(2000, test);
|
||||
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,36 +1,38 @@
|
||||
import "./setup-test-env.js";
|
||||
import { Fuzzer } from "./fuzz-lib.mjs";
|
||||
import { normalizeAndAugmentMagnet } from "../../js/magnetUtils.js";
|
||||
|
||||
// Mock global dependencies if needed
|
||||
if (typeof global.window === "undefined") {
|
||||
global.window = {
|
||||
location: { protocol: "https:" }
|
||||
};
|
||||
}
|
||||
|
||||
const fuzzer = new Fuzzer("magnet-utils");
|
||||
|
||||
async function test(fuzzer, state) {
|
||||
const rawValue = fuzzer.randBool() ?
|
||||
`magnet:?xt=urn:btih:${fuzzer.randString(40, "0123456789abcdef")}&dn=${fuzzer.randString(10)}` :
|
||||
fuzzer.randUnicodeString(100);
|
||||
async function fuzzTest(fuzzer, state) {
|
||||
// Generate inputs
|
||||
const rawValue = fuzzer.randBool() ? fuzzer.randString(100) : "magnet:?xt=urn:btih:" + fuzzer.randString(40, "0123456789abcdef");
|
||||
|
||||
const options = {
|
||||
webSeed: fuzzer.randBool() ? fuzzer.randString(50) : fuzzer.randArray(() => fuzzer.randString(50), 0, 3),
|
||||
torrentUrl: fuzzer.randBool() ? fuzzer.randString(50) : undefined,
|
||||
xs: fuzzer.randBool() ? fuzzer.randString(50) : undefined,
|
||||
extraTrackers: fuzzer.randArray(() => fuzzer.randString(30), 0, 5),
|
||||
appProtocol: fuzzer.pick(["http:", "https:", "magnet:", "unknown:"]),
|
||||
logger: () => {} // No-op logger
|
||||
};
|
||||
// Inject garbage into magnet link
|
||||
let malformedValue = rawValue;
|
||||
if (fuzzer.randBool()) {
|
||||
// url encode some chars
|
||||
malformedValue = encodeURIComponent(rawValue);
|
||||
}
|
||||
|
||||
// Sometimes pass garbage options
|
||||
if (fuzzer.randBool()) {
|
||||
options.webSeed = fuzzer.randJSON();
|
||||
}
|
||||
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, options };
|
||||
normalizeAndAugmentMagnet(rawValue, options);
|
||||
state.input = {
|
||||
rawValue: malformedValue,
|
||||
options
|
||||
};
|
||||
|
||||
try {
|
||||
normalizeAndAugmentMagnet(malformedValue, options);
|
||||
} catch (err) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
fuzzer.runFuzzLoop(5000, test);
|
||||
fuzzer.runFuzzLoop(5000, fuzzTest).catch(console.error);
|
||||
@@ -1,106 +1,84 @@
|
||||
import "./setup-test-env.js";
|
||||
import { Fuzzer } from "./fuzz-lib.mjs";
|
||||
import * as schemas from "../../js/nostrEventSchemas.js";
|
||||
|
||||
// Mock global dependencies if needed
|
||||
if (typeof global.window === "undefined") {
|
||||
global.window = {};
|
||||
}
|
||||
|
||||
const fuzzer = new Fuzzer("nostr-schemas");
|
||||
|
||||
// Builders take a params object.
|
||||
const builders = [
|
||||
"buildVideoPostEvent",
|
||||
"buildVideoMirrorEvent",
|
||||
"buildRepostEvent",
|
||||
"buildRelayListEvent",
|
||||
"buildDmRelayListEvent",
|
||||
"buildProfileMetadataEvent",
|
||||
"buildMuteListEvent",
|
||||
"buildDmAttachmentEvent",
|
||||
"buildDmReadReceiptEvent",
|
||||
"buildDmTypingIndicatorEvent",
|
||||
"buildViewEvent",
|
||||
"buildZapRequestEvent",
|
||||
"buildReactionEvent",
|
||||
"buildCommentEvent",
|
||||
"buildWatchHistoryEvent",
|
||||
"buildSubscriptionListEvent",
|
||||
"buildBlockListEvent",
|
||||
"buildHashtagPreferenceEvent",
|
||||
// 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 test(fuzzer, state) {
|
||||
const action = fuzzer.randInt(0, 3);
|
||||
let input = {};
|
||||
async function fuzzTest(fuzzer, state) {
|
||||
// 1. Fuzz Builder Functions
|
||||
const builder = fuzzer.pick(BUILDERS);
|
||||
|
||||
if (action === 0) {
|
||||
// Fuzz sanitizeAdditionalTags
|
||||
const tags = fuzzer.randArray(() => {
|
||||
if (fuzzer.randBool()) {
|
||||
return fuzzer.randArray(() => fuzzer.randString(10), 0, 5);
|
||||
} else {
|
||||
return fuzzer.randJSON(); // Garbage
|
||||
// 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;
|
||||
}
|
||||
}, 0, 20);
|
||||
input = { target: "sanitizeAdditionalTags", tags };
|
||||
state.input = input;
|
||||
schemas.sanitizeAdditionalTags(tags);
|
||||
}
|
||||
|
||||
} else if (action === 1) {
|
||||
// Fuzz validateEventAgainstSchema
|
||||
const types = Object.keys(schemas.NOTE_TYPES);
|
||||
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 event = {
|
||||
kind: fuzzer.randInt(0, 40000),
|
||||
content: fuzzer.randUnicodeString(100),
|
||||
tags: fuzzer.randArray(() => fuzzer.randArray(() => fuzzer.randString(10), 0, 5), 0, 10),
|
||||
pubkey: fuzzer.randString(64, "0123456789abcdef"),
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
const randomEvent = fuzzer.randJSON();
|
||||
|
||||
state.input = {
|
||||
target: "validateEventAgainstSchema",
|
||||
type: type,
|
||||
event: randomEvent
|
||||
};
|
||||
|
||||
// Mutate event
|
||||
if (fuzzer.randBool()) event.content = fuzzer.randJSON();
|
||||
if (fuzzer.randBool()) event.tags = "not an array";
|
||||
|
||||
input = { target: "validateEventAgainstSchema", type, event };
|
||||
state.input = input;
|
||||
schemas.validateEventAgainstSchema(type, event);
|
||||
|
||||
} else if (action === 2) {
|
||||
// Fuzz Builders
|
||||
const builderName = fuzzer.pick(builders);
|
||||
const builderFn = schemas[builderName];
|
||||
|
||||
// Generate random params
|
||||
const params = fuzzer.randObject({
|
||||
pubkey: () => fuzzer.randString(64, "0123456789abcdef"),
|
||||
created_at: () => Math.floor(Date.now() / 1000),
|
||||
content: () => fuzzer.randUnicodeString(50),
|
||||
additionalTags: () => fuzzer.randArray(() => [fuzzer.randString(5), fuzzer.randString(10)], 0, 5),
|
||||
dTagValue: () => fuzzer.randString(10),
|
||||
eventId: () => fuzzer.randString(64, "0123456789abcdef"),
|
||||
// Add more random keys that might be expected or unexpected
|
||||
[fuzzer.randString(5)]: () => fuzzer.randJSON()
|
||||
});
|
||||
|
||||
input = { target: builderName, params };
|
||||
state.input = input;
|
||||
|
||||
if (typeof builderFn === "function") {
|
||||
builderFn(params);
|
||||
}
|
||||
} else if (action === 3) {
|
||||
// buildAdminListEvent special case
|
||||
const listKey = fuzzer.pick(["moderation", "editors", "whitelist", "blacklist", fuzzer.randString(5)]);
|
||||
const params = {
|
||||
pubkey: fuzzer.randString(64, "0123456789abcdef"),
|
||||
hexPubkeys: fuzzer.randArray(() => fuzzer.randString(64, "0123456789abcdef"), 0, 5)
|
||||
};
|
||||
input = { target: "buildAdminListEvent", listKey, params };
|
||||
state.input = input;
|
||||
schemas.buildAdminListEvent(listKey, params);
|
||||
schemas.validateEventAgainstSchema(type, randomEvent);
|
||||
}
|
||||
}
|
||||
|
||||
fuzzer.runFuzzLoop(2000, test);
|
||||
// Run for 5000 iterations
|
||||
fuzzer.runFuzzLoop(5000, fuzzTest).catch(console.error);
|
||||
Reference in new issue
Block a user