Fuzz testing and hardening for nostrEventSchemas and magnetUtils

- Added fuzzing scripts for nostrEventSchemas, dmDecryptor, and magnetUtils.
- Fixed crashes in nostrEventSchemas related to unsafe stringification of content.
- Fixed crash in magnetUtils when options argument is null.
- Added safeStringify helper in nostrEventSchemas.js.
- Included fuzzing reports and reproducers.

Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
google-labs-jules[bot]andPR0M3TH3AN committed 2026-01-23 16:12:42 +00:00
1 parent 725e3eb28b
commit 932e57c724
27 files changed
+1012 -410

No files matched your search

@@ -0,0 +1,13 @@
To run:
```bash
node --import ../../../tests/test-helpers/setup-localstorage.mjs repro.mjs
```
Error: Explosion!
Stack: Error: Explosion!
at Object.toString (file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:103:29)
at String (<anonymous>)
at buildVideoMirrorEvent (file:///app/js/nostrEventSchemas.js:878:54)
at fuzzFunction (file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:210:14)
at file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:244:3
@@ -0,0 +1,17 @@
import { buildVideoMirrorEvent } from '../../../js/nostrEventSchemas.js';
const args = [
{
"content": {}
}
];
console.log('Running reproduction for buildVideoMirrorEvent...');
try {
buildVideoMirrorEvent(...args);
console.log('No crash reproduced.');
} catch (error) {
console.log('Crash reproduced:');
console.error(error);
}
@@ -0,0 +1,12 @@
To run:
```bash
node --import ../../../tests/test-helpers/setup-localstorage.mjs repro.mjs
```
Error: Cannot convert object to primitive value
Stack: TypeError: Cannot convert object to primitive value
at String (<anonymous>)
at buildVideoMirrorEvent (file:///app/js/nostrEventSchemas.js:878:54)
at fuzzFunction (file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:210:14)
at file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:244:3
@@ -0,0 +1,17 @@
import { buildVideoMirrorEvent } from '../../../js/nostrEventSchemas.js';
const args = [
{
"content": {}
}
];
console.log('Running reproduction for buildVideoMirrorEvent...');
try {
buildVideoMirrorEvent(...args);
console.log('No crash reproduced.');
} catch (error) {
console.log('Crash reproduced:');
console.error(error);
}
@@ -0,0 +1,13 @@
To run:
```bash
node --import ../../../tests/test-helpers/setup-localstorage.mjs repro.mjs
```
Error: Explosion!
Stack: Error: Explosion!
at Object.toString (file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:103:29)
at String (<anonymous>)
at buildWatchHistoryEvent (file:///app/js/nostrEventSchemas.js:2147:54)
at fuzzFunction (file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:210:14)
at file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:244:3
@@ -0,0 +1,17 @@
import { buildWatchHistoryEvent } from '../../../js/nostrEventSchemas.js';
const args = [
{
"content": {}
}
];
console.log('Running reproduction for buildWatchHistoryEvent...');
try {
buildWatchHistoryEvent(...args);
console.log('No crash reproduced.');
} catch (error) {
console.log('Crash reproduced:');
console.error(error);
}
@@ -0,0 +1,12 @@
To run:
```bash
node --import ../../../tests/test-helpers/setup-localstorage.mjs repro.mjs
```
Error: Cannot convert object to primitive value
Stack: TypeError: Cannot convert object to primitive value
at String (<anonymous>)
at buildWatchHistoryEvent (file:///app/js/nostrEventSchemas.js:2147:54)
at fuzzFunction (file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:210:14)
at file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:244:3
@@ -0,0 +1,17 @@
import { buildWatchHistoryEvent } from '../../../js/nostrEventSchemas.js';
const args = [
{
"content": {}
}
];
console.log('Running reproduction for buildWatchHistoryEvent...');
try {
buildWatchHistoryEvent(...args);
console.log('No crash reproduced.');
} catch (error) {
console.log('Crash reproduced:');
console.error(error);
}
@@ -0,0 +1,13 @@
To run:
```bash
node --import ../../../tests/test-helpers/setup-localstorage.mjs repro.mjs
```
Error: Explosion!
Stack: Error: Explosion!
at Object.toString (file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:103:29)
at String (<anonymous>)
at buildSubscriptionListEvent (file:///app/js/nostrEventSchemas.js:2183:54)
at fuzzFunction (file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:210:14)
at file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:244:3
@@ -0,0 +1,17 @@
import { buildSubscriptionListEvent } from '../../../js/nostrEventSchemas.js';
const args = [
{
"content": {}
}
];
console.log('Running reproduction for buildSubscriptionListEvent...');
try {
buildSubscriptionListEvent(...args);
console.log('No crash reproduced.');
} catch (error) {
console.log('Crash reproduced:');
console.error(error);
}
@@ -0,0 +1,12 @@
To run:
```bash
node --import ../../../tests/test-helpers/setup-localstorage.mjs repro.mjs
```
Error: Cannot convert object to primitive value
Stack: TypeError: Cannot convert object to primitive value
at String (<anonymous>)
at buildSubscriptionListEvent (file:///app/js/nostrEventSchemas.js:2183:54)
at fuzzFunction (file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:210:14)
at file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:244:3
@@ -0,0 +1,17 @@
import { buildSubscriptionListEvent } from '../../../js/nostrEventSchemas.js';
const args = [
{
"content": {}
}
];
console.log('Running reproduction for buildSubscriptionListEvent...');
try {
buildSubscriptionListEvent(...args);
console.log('No crash reproduced.');
} catch (error) {
console.log('Crash reproduced:');
console.error(error);
}
@@ -0,0 +1,13 @@
To run:
```bash
node --import ../../../tests/test-helpers/setup-localstorage.mjs repro.mjs
```
Error: Explosion!
Stack: Error: Explosion!
at Object.toString (file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:103:29)
at String (<anonymous>)
at buildBlockListEvent (file:///app/js/nostrEventSchemas.js:2219:54)
at fuzzFunction (file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:210:14)
at file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:244:3
@@ -0,0 +1,17 @@
import { buildBlockListEvent } from '../../../js/nostrEventSchemas.js';
const args = [
{
"content": {}
}
];
console.log('Running reproduction for buildBlockListEvent...');
try {
buildBlockListEvent(...args);
console.log('No crash reproduced.');
} catch (error) {
console.log('Crash reproduced:');
console.error(error);
}
@@ -0,0 +1,12 @@
To run:
```bash
node --import ../../../tests/test-helpers/setup-localstorage.mjs repro.mjs
```
Error: Cannot convert object to primitive value
Stack: TypeError: Cannot convert object to primitive value
at String (<anonymous>)
at buildBlockListEvent (file:///app/js/nostrEventSchemas.js:2219:54)
at fuzzFunction (file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:210:14)
at file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:244:3
@@ -0,0 +1,17 @@
import { buildBlockListEvent } from '../../../js/nostrEventSchemas.js';
const args = [
{
"content": {}
}
];
console.log('Running reproduction for buildBlockListEvent...');
try {
buildBlockListEvent(...args);
console.log('No crash reproduced.');
} catch (error) {
console.log('Crash reproduced:');
console.error(error);
}
@@ -0,0 +1,13 @@
To run:
```bash
node --import ../../../tests/test-helpers/setup-localstorage.mjs repro.mjs
```
Error: Explosion!
Stack: Error: Explosion!
at Object.toString (file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:103:29)
at String (<anonymous>)
at buildHashtagPreferenceEvent (file:///app/js/nostrEventSchemas.js:2249:54)
at fuzzFunction (file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:210:14)
at file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:244:3
@@ -0,0 +1,17 @@
import { buildHashtagPreferenceEvent } from '../../../js/nostrEventSchemas.js';
const args = [
{
"content": {}
}
];
console.log('Running reproduction for buildHashtagPreferenceEvent...');
try {
buildHashtagPreferenceEvent(...args);
console.log('No crash reproduced.');
} catch (error) {
console.log('Crash reproduced:');
console.error(error);
}
@@ -0,0 +1,12 @@
To run:
```bash
node --import ../../../tests/test-helpers/setup-localstorage.mjs repro.mjs
```
Error: Cannot convert object to primitive value
Stack: TypeError: Cannot convert object to primitive value
at String (<anonymous>)
at buildHashtagPreferenceEvent (file:///app/js/nostrEventSchemas.js:2249:54)
at fuzzFunction (file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:210:14)
at file:///app/scripts/agent/fuzz-nostrEventSchemas.mjs:244:3
@@ -0,0 +1,17 @@
import { buildHashtagPreferenceEvent } from '../../../js/nostrEventSchemas.js';
const args = [
{
"content": {}
}
];
console.log('Running reproduction for buildHashtagPreferenceEvent...');
try {
buildHashtagPreferenceEvent(...args);
console.log('No crash reproduced.');
} catch (error) {
console.log('Crash reproduced:');
console.error(error);
}
@@ -0,0 +1,14 @@
To run:
```bash
node --import ../../../tests/test-helpers/setup-localstorage.mjs repro.mjs
```
Error: Cannot destructure property 'webSeed' of '(intermediate value)(intermediate value)(intermediate value)' as it is null.
Stack: TypeError: Cannot destructure property 'webSeed' of '(intermediate value)(intermediate value)(intermediate value)' as it is null.
at normalizeAndAugmentMagnet (file:///app/js/magnetUtils.js:20:5)
at fuzzNormalize (file:///app/scripts/agent/fuzz-magnetUtils.mjs:175:13)
at file:///app/scripts/agent/fuzz-magnetUtils.mjs:205:1
at ModuleJob.run (node:internal/modules/esm/module_job:343:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:665:26)
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:117:5)
@@ -0,0 +1,16 @@
import { normalizeAndAugmentMagnet } from '../../../js/magnetUtils.js';
const args = [
"magnet:?xt=urn:btih:0000000000000000000000000000000000000000",
null
];
console.log('Running reproduction for normalizeAndAugmentMagnet...');
try {
normalizeAndAugmentMagnet(...args);
console.log('No crash reproduced.');
} catch (error) {
console.log('Crash reproduced:');
console.error(error);
}
+5 -3
View File
@@ -16,15 +16,17 @@ export const safeDecodeMagnet = sharedSafeDecodeMagnet;
export function normalizeAndAugmentMagnet(
rawValue,
{
options = {}
) {
const {
webSeed,
torrentUrl,
xs,
extraTrackers = [],
logger,
appProtocol,
} = {}
) {
} = options || {};
const safeExtraTrackers = Array.isArray(extraTrackers) ? extraTrackers : [];
const {
+19 -5
View File
@@ -60,6 +60,20 @@ let cachedUtf8Encoder = null;
const ADDITIONAL_POINTER_TAGS = new Set(["p", "e"]);
const HEX_32_BYTE_REGEX = /^[0-9a-f]{64}$/i;
function safeStringify(value) {
if (typeof value === "string") {
return value;
}
if (value === null || value === undefined) {
return "";
}
try {
return String(value);
} catch (err) {
return "";
}
}
function normalizePointerIdentifier(value) {
if (typeof value !== "string") {
return "";
@@ -875,7 +889,7 @@ export function buildVideoMirrorEvent(params) {
pubkey,
created_at,
tags: combinedTags,
content: typeof content === "string" ? content : String(content ?? ""),
content: safeStringify(content),
};
if (isDevMode) {
@@ -2144,7 +2158,7 @@ export function buildWatchHistoryEvent(params) {
pubkey,
created_at: Number.isFinite(created_at) ? created_at : Math.floor(Date.now() / 1000),
tags,
content: typeof content === "string" ? content : String(content ?? ""),
content: safeStringify(content),
};
if (isDevMode) {
@@ -2180,7 +2194,7 @@ export function buildSubscriptionListEvent(params) {
pubkey,
created_at,
tags,
content: typeof content === "string" ? content : String(content ?? ""),
content: safeStringify(content),
};
if (isDevMode) {
@@ -2216,7 +2230,7 @@ export function buildBlockListEvent(params) {
pubkey,
created_at,
tags,
content: typeof content === "string" ? content : String(content ?? ""),
content: safeStringify(content),
};
if (isDevMode) {
@@ -2246,7 +2260,7 @@ export function buildHashtagPreferenceEvent(params) {
pubkey,
created_at,
tags,
content: typeof content === "string" ? content : String(content ?? ""),
content: safeStringify(content),
};
if (isDevMode) {
+228 -55
View File
@@ -1,63 +1,236 @@
import { decryptDM } from "../../js/dmDecryptor.js";
import { runFuzzer, randomString, randomInt, randomJSON, randomHex, randomBoolean, randomItem } from "./fuzz-shared.mjs";
import fs from 'node:fs';
import path from 'node:path';
import { decryptDM } from '../../js/dmDecryptor.js';
async function fuzzDmDecryptor(iteration) {
const schemes = ["nip04", "nip44", "nip44_v2", randomString(5)];
// Setup artifacts and reproducers
const ARTIFACTS_DIR = 'artifacts';
const REPRODUCERS_DIR = 'examples/reproducers';
const REPORT_FILE = path.join(ARTIFACTS_DIR, 'fuzz-report-dmDecryptor.json');
const mockDecryptor = (scheme) => ({
scheme,
decrypt: async (pubkey, ciphertext, options) => {
// Simulate random failures
if (Math.random() < 0.1) throw new Error("Decryption failed");
if (!fs.existsSync(ARTIFACTS_DIR)) fs.mkdirSync(ARTIFACTS_DIR, { recursive: true });
if (!fs.existsSync(REPRODUCERS_DIR)) fs.mkdirSync(REPRODUCERS_DIR, { recursive: true });
// 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
const BIG_NAUGHTY_LIST = [
undefined,
null,
0,
1,
-1,
NaN,
Infinity,
-Infinity,
true,
false,
'',
' ',
'\n',
'\t',
'\r',
'\0',
'a',
'0',
'[]',
'{}',
'null',
'undefined',
'NaN',
'Infinity',
'<>',
'\'',
'"',
'`',
'\\',
'/',
'.',
',',
':',
';',
'!',
'?',
'@',
'#',
'$',
'%',
'^',
'&',
'*',
'(',
')',
'-',
'_',
'+',
'=',
'[',
']',
'{',
'}',
'|',
'~',
'💩',
'Hello 🌍',
'\uD800',
'\uDFFF',
'\u0000',
'\uFFFF',
'\uFFFD',
Array(1000).fill('a').join(''),
Object.create(null),
[],
{},
[null],
[undefined],
[''],
{ a: 1 },
];
const recursiveObj = {};
recursiveObj.self = recursiveObj;
BIG_NAUGHTY_LIST.push(recursiveObj);
const report = {
target: 'dmDecryptor',
timestamp: new Date().toISOString(),
failures: [],
};
function logFailure(funcName, args, error) {
const hash = Math.random().toString(36).substring(2, 10);
const reproDir = path.join(REPRODUCERS_DIR, `issue-${Date.now()}-${hash}`);
fs.mkdirSync(reproDir, { recursive: true });
// Simplified serialization for repro script
const serialize = (obj) => {
try {
return JSON.stringify(obj, (key, value) => {
if (value === undefined) return '__UNDEFINED__';
if (Number.isNaN(value)) return '__NAN__';
if (value === Infinity) return '__INFINITY__';
if (typeof value === 'function') return value.toString();
return value;
}, 2).replace(/"__UNDEFINED__"/g, 'undefined').replace(/"__NAN__"/g, 'NaN').replace(/"__INFINITY__"/g, 'Infinity');
} catch (e) {
return '{}'; // Fallback
}
};
const reproScript = `
import { ${funcName} } from '../../../js/dmDecryptor.js';
const args = ${serialize(args)};
// Rehydrate functions if any
args.forEach(arg => {
if (arg && arg.decryptors) {
arg.decryptors.forEach(d => {
// This is a rough approximation, real repro might need manual adjustment if functions were complex
// But for fuzzing we usually pass simple mock functions
d.decrypt = async () => "decrypted";
});
}
});
console.log('Running reproduction for ${funcName}...');
try {
await ${funcName}(...args);
console.log('No crash reproduced.');
} catch (error) {
console.log('Crash reproduced:');
console.error(error);
}
`;
fs.writeFileSync(path.join(reproDir, 'repro.mjs'), reproScript);
fs.writeFileSync(path.join(reproDir, 'README.md'), `To run:\n\n\`\`\`bash\nnode --import ../../../tests/test-helpers/setup-localstorage.mjs repro.mjs\n\`\`\`\n\nError: ${error.message}\nStack: ${error.stack}`);
report.failures.push({
function: funcName,
args: args.map(a => {
try {
return JSON.stringify(a);
} catch {
return '[Circular/Unserializable]';
}
}),
error: {
message: error.message,
stack: error.stack,
},
source: "mock-fuzzer",
priority: randomInt(0, 10),
supportsGiftWrap: scheme.startsWith("nip44")
reproDir,
});
const decryptors = [];
const numDecryptors = randomInt(0, 3);
for (let i = 0; i < numDecryptors; i++) {
decryptors.push(mockDecryptor(randomItem(schemes)));
}
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)
};
// 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;
console.error(`FAILURE in ${funcName}: ${error.message}`);
}
runFuzzer("dmDecryptor", fuzzDmDecryptor, 5000);
async function fuzzDecryptDM() {
console.log(`Fuzzing decryptDM...`);
const mockDecryptor = {
scheme: 'nip44',
decrypt: async (pubkey, ciphertext) => {
if (ciphertext === 'FAIL') throw new Error('Decryption failed intentionally');
if (ciphertext === 'JUNK') return 'NOT JSON';
if (ciphertext === 'EMPTY') return '';
return JSON.stringify({ content: 'secret message', kind: 1, created_at: 12345 });
}
};
const naughtyDecryptors = [
null,
undefined,
{},
{ decrypt: null },
{ decrypt: 'not a function' },
{ decrypt: async () => { throw new Error('Boom'); } },
{ decrypt: async () => 123 }, // returns non-string
];
// Test 1: Fuzz event argument
for (const input of BIG_NAUGHTY_LIST) {
try {
await decryptDM(input, { decryptors: [mockDecryptor] });
} catch (error) {
logFailure('decryptDM', [input, { decryptors: ['[MockDecryptor]'] }], error);
}
}
// Test 2: Fuzz context argument
const validEvent = { kind: 4, content: 'ciphertext', pubkey: '00'.repeat(32), tags: [] };
for (const input of BIG_NAUGHTY_LIST) {
try {
await decryptDM(validEvent, input);
} catch (error) {
logFailure('decryptDM', [validEvent, input], error);
}
}
// Test 3: Fuzz decryptors list
for (const input of BIG_NAUGHTY_LIST) {
try {
await decryptDM(validEvent, { decryptors: input });
} catch (error) {
logFailure('decryptDM', [validEvent, { decryptors: input }], error);
}
}
// Test 4: Naughty decryptors
for (const badDecryptor of naughtyDecryptors) {
try {
await decryptDM(validEvent, { decryptors: [badDecryptor] });
} catch (error) {
logFailure('decryptDM', [validEvent, { decryptors: [badDecryptor] }], error);
}
}
// Test 5: Gift Wrap specific fuzzing (kind 1059)
const giftWrapEvent = { kind: 1059, content: 'ciphertext', pubkey: '00'.repeat(32), tags: [] };
try {
await decryptDM(giftWrapEvent, { decryptors: [mockDecryptor] });
} catch (error) {
logFailure('decryptDM', [giftWrapEvent, { decryptors: ['[MockDecryptor]'] }], error);
}
}
await fuzzDecryptDM();
fs.writeFileSync(REPORT_FILE, JSON.stringify(report, null, 2));
console.log(`Fuzzing complete. Report saved to ${REPORT_FILE}`);
+197 -35
View File
@@ -1,47 +1,209 @@
import { normalizeAndAugmentMagnet } from "../../js/magnetUtils.js";
import { runFuzzer, randomString, randomInt, randomJSON, randomHex, randomBoolean, randomItem } from "./fuzz-shared.mjs";
import fs from 'node:fs';
import path from 'node:path';
import { normalizeAndAugmentMagnet, safeDecodeMagnet } from '../../js/magnetUtils.js';
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),
""
];
// Setup artifacts and reproducers
const ARTIFACTS_DIR = 'artifacts';
const REPRODUCERS_DIR = 'examples/reproducers';
const REPORT_FILE = path.join(ARTIFACTS_DIR, 'fuzz-report-magnetUtils.json');
const rawValue = Math.random() < 0.8 ? randomItem(baseMagnets) : randomString(100);
if (!fs.existsSync(ARTIFACTS_DIR)) fs.mkdirSync(ARTIFACTS_DIR, { recursive: true });
if (!fs.existsSync(REPRODUCERS_DIR)) fs.mkdirSync(REPRODUCERS_DIR, { recursive: true });
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)])
const BIG_NAUGHTY_LIST = [
undefined,
null,
0,
1,
-1,
NaN,
Infinity,
-Infinity,
true,
false,
'',
' ',
'\n',
'\t',
'\r',
'\0',
'a',
'0',
'[]',
'{}',
'null',
'undefined',
'NaN',
'Infinity',
'<>',
'\'',
'"',
'`',
'\\',
'/',
'.',
',',
':',
';',
'!',
'?',
'@',
'#',
'$',
'%',
'^',
'&',
'*',
'(',
')',
'-',
'_',
'+',
'=',
'[',
']',
'{',
'}',
'|',
'~',
'magnet:?',
'magnet:?xt=urn:btih:',
'magnet:?xt=urn:btih:123',
'magnet:?xt=urn:btih:0000000000000000000000000000000000000000',
'magnet:?tr=udp://tracker.opentrackr.org:1337/announce',
'%',
'%25',
'%%',
'http://example.com',
'wss://tracker.example.com',
'\uD800',
'\uDFFF',
'\u0000',
'\uFFFF',
'\uFFFD',
Array(1000).fill('a').join(''),
Object.create(null),
[],
{},
[null],
[undefined],
[''],
{ a: 1 },
];
const recursiveObj = {};
recursiveObj.self = recursiveObj;
BIG_NAUGHTY_LIST.push(recursiveObj);
const report = {
target: 'magnetUtils',
timestamp: new Date().toISOString(),
failures: [],
};
function logFailure(funcName, args, error) {
const hash = Math.random().toString(36).substring(2, 10);
const reproDir = path.join(REPRODUCERS_DIR, `issue-${Date.now()}-${hash}`);
fs.mkdirSync(reproDir, { recursive: true });
const serialize = (obj) => {
try {
return JSON.stringify(obj, (key, value) => {
if (value === undefined) return '__UNDEFINED__';
if (Number.isNaN(value)) return '__NAN__';
if (value === Infinity) return '__INFINITY__';
return value;
}, 2).replace(/"__UNDEFINED__"/g, 'undefined').replace(/"__NAN__"/g, 'NaN').replace(/"__INFINITY__"/g, 'Infinity');
} catch (e) {
return '{}';
}
};
// Malformed options
if (Math.random() < 0.1) {
options.webSeed = 123;
}
if (Math.random() < 0.1) {
options.extraTrackers = "string";
}
const reproScript = `
import { ${funcName} } from '../../../js/magnetUtils.js';
const input = {
rawValue,
options
};
const args = ${serialize(args)};
// Mock logger to avoid spamming console
options.logger = () => {};
console.log('Running reproduction for ${funcName}...');
try {
${funcName}(...args);
console.log('No crash reproduced.');
} catch (error) {
console.log('Crash reproduced:');
console.error(error);
}
`;
normalizeAndAugmentMagnet(rawValue, options);
fs.writeFileSync(path.join(reproDir, 'repro.mjs'), reproScript);
fs.writeFileSync(path.join(reproDir, 'README.md'), `To run:\n\n\`\`\`bash\nnode --import ../../../tests/test-helpers/setup-localstorage.mjs repro.mjs\n\`\`\`\n\nError: ${error.message}\nStack: ${error.stack}`);
return input;
report.failures.push({
function: funcName,
args: args.map(a => {
try {
return JSON.stringify(a);
} catch {
return '[Circular/Unserializable]';
}
}),
error: {
message: error.message,
stack: error.stack,
},
reproDir,
});
console.error(`FAILURE in ${funcName}: ${error.message}`);
}
runFuzzer("magnetUtils", fuzzMagnetUtils, 10000); // Fast, so more iterations
function fuzzNormalize() {
console.log(`Fuzzing normalizeAndAugmentMagnet...`);
// Test 1: Fuzz rawValue
for (const input of BIG_NAUGHTY_LIST) {
try {
normalizeAndAugmentMagnet(input);
} catch (error) {
logFailure('normalizeAndAugmentMagnet', [input], error);
}
}
// Test 2: Fuzz options
const validMagnet = 'magnet:?xt=urn:btih:0000000000000000000000000000000000000000';
for (const input of BIG_NAUGHTY_LIST) {
try {
normalizeAndAugmentMagnet(validMagnet, input);
} catch (error) {
logFailure('normalizeAndAugmentMagnet', [validMagnet, input], error);
}
}
// Test 3: Fuzz specific option fields
const optionKeys = ['webSeed', 'torrentUrl', 'xs', 'extraTrackers', 'appProtocol'];
for (const key of optionKeys) {
for (const input of BIG_NAUGHTY_LIST) {
try {
normalizeAndAugmentMagnet(validMagnet, { [key]: input });
} catch (error) {
logFailure('normalizeAndAugmentMagnet', [validMagnet, { [key]: input }], error);
}
}
}
}
function fuzzSafeDecode() {
console.log(`Fuzzing safeDecodeMagnet...`);
for (const input of BIG_NAUGHTY_LIST) {
try {
safeDecodeMagnet(input);
} catch (error) {
logFailure('safeDecodeMagnet', [input], error);
}
}
}
fuzzNormalize();
fuzzSafeDecode();
fs.writeFileSync(REPORT_FILE, JSON.stringify(report, null, 2));
console.log(`Fuzzing complete. Report saved to ${REPORT_FILE}`);
+238 -312
View File
@@ -1,322 +1,248 @@
import * as schemas from "../../js/nostrEventSchemas.js";
import { runFuzzer, randomString, randomInt, randomJSON, randomHex, randomBoolean, randomItem } from "./fuzz-shared.mjs";
import fs from 'node:fs';
import path from 'node:path';
import {
buildVideoPostEvent,
buildVideoMirrorEvent,
buildRepostEvent,
buildShareEvent,
buildRelayListEvent,
buildDmRelayListEvent,
buildProfileMetadataEvent,
buildMuteListEvent,
buildDeletionEvent,
buildLegacyDirectMessageEvent,
buildDmAttachmentEvent,
buildDmReadReceiptEvent,
buildDmTypingIndicatorEvent,
buildViewEvent,
buildZapRequestEvent,
buildReactionEvent,
buildCommentEvent,
buildWatchHistoryEvent,
buildSubscriptionListEvent,
buildBlockListEvent,
buildHashtagPreferenceEvent,
sanitizeAdditionalTags,
} from '../../js/nostrEventSchemas.js';
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"
];
// Setup artifacts and reproducers
const ARTIFACTS_DIR = 'artifacts';
const REPRODUCERS_DIR = 'examples/reproducers';
const REPORT_FILE = path.join(ARTIFACTS_DIR, 'fuzz-report-nostrEventSchemas.json');
const action = actions[iteration % actions.length];
// Introduce randomness within the cycle
// const action = randomItem(actions);
if (!fs.existsSync(ARTIFACTS_DIR)) fs.mkdirSync(ARTIFACTS_DIR, { recursive: true });
if (!fs.existsSync(REPRODUCERS_DIR)) fs.mkdirSync(REPRODUCERS_DIR, { recursive: true });
let input = { action };
const BIG_NAUGHTY_LIST = [
undefined,
null,
0,
1,
-1,
NaN,
Infinity,
-Infinity,
true,
false,
'',
' ',
'\n',
'\t',
'\r',
'\0',
'a',
'0',
'[]',
'{}',
'null',
'undefined',
'NaN',
'Infinity',
'<>',
'\'',
'"',
'`',
'\\',
'/',
'.',
',',
':',
';',
'!',
'?',
'@',
'#',
'$',
'%',
'^',
'&',
'*',
'(',
')',
'-',
'_',
'+',
'=',
'[',
']',
'{',
'}',
'|',
'~',
'💩', // Emoji
'Hello 🌍', // Text with emoji
'Zalgo text: Hͥ̽ͣ̃ͤ́eͧ̈́lͥlͦo', // Zalgo
'\uD800', // Lone surrogate
'\uDFFF',
'\uD83D\uDCA9', // Valid surrogate pair (Pile of Poo)
'\u0000', // Null byte
'\uFFFF',
'\uFFFD', // Replacement character
Array(1000).fill('a').join(''), // Long string
{ toString: () => { throw new Error('Explosion!'); } }, // Object that throws on toString
{ toJSON: () => { throw new Error('JSON Explosion!'); } }, // Object that throws on toJSON
Object.create(null), // Null prototype object
[],
{},
[null],
[undefined],
[''],
{ a: 1 },
];
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;
}
// Add a recursive object
const recursiveObj = {};
recursiveObj.self = recursiveObj;
BIG_NAUGHTY_LIST.push(recursiveObj);
const report = {
target: 'nostrEventSchemas',
timestamp: new Date().toISOString(),
failures: [],
};
function logFailure(funcName, args, error) {
const hash = Math.random().toString(36).substring(2, 10);
const reproDir = path.join(REPRODUCERS_DIR, `issue-${Date.now()}-${hash}`);
fs.mkdirSync(reproDir, { recursive: true });
const reproScript = `
import { ${funcName} } from '../../../js/nostrEventSchemas.js';
const args = ${JSON.stringify(args, (key, value) => {
if (value === undefined) return '__UNDEFINED__';
if (Number.isNaN(value)) return '__NAN__';
if (value === Infinity) return '__INFINITY__';
if (value === -Infinity) return '__NEG_INFINITY__';
if (typeof value === 'object' && value !== null) {
// Handle circular references for JSON stringify
const seen = new WeakSet();
const circularReplacer = (k, v) => {
if (typeof v === 'object' && v !== null) {
if (seen.has(v)) {
return '[Circular]';
}
seen.add(v);
}
return v;
};
// Simple circular check handling for top level if needed, but here we just try-catch or simplify
try {
return value;
} catch(e) {
return '[Unserializable]';
}
}
return value;
}, 2).replace(/"__UNDEFINED__"/g, 'undefined').replace(/"__NAN__"/g, 'NaN').replace(/"__INFINITY__"/g, 'Infinity').replace(/"__NEG_INFINITY__"/g, '-Infinity')};
return input;
} catch (error) {
throw error;
console.log('Running reproduction for ${funcName}...');
try {
${funcName}(...args);
console.log('No crash reproduced.');
} catch (error) {
console.log('Crash reproduced:');
console.error(error);
}
`;
fs.writeFileSync(path.join(reproDir, 'repro.mjs'), reproScript);
fs.writeFileSync(path.join(reproDir, 'README.md'), `To run:\n\n\`\`\`bash\nnode --import ../../../tests/test-helpers/setup-localstorage.mjs repro.mjs\n\`\`\`\n\nError: ${error.message}\nStack: ${error.stack}`);
report.failures.push({
function: funcName,
args: args.map(a => {
try {
return JSON.stringify(a);
} catch {
return '[Circular/Unserializable]';
}
}),
error: {
message: error.message,
stack: error.stack,
},
reproDir,
});
console.error(`FAILURE in ${funcName}: ${error.message}`);
}
function fuzzFunction(func, funcName) {
console.log(`Fuzzing ${funcName}...`);
// Test 1: Single argument fuzzing
for (const input of BIG_NAUGHTY_LIST) {
try {
func(input);
} catch (error) {
logFailure(funcName, [input], error);
}
}
// Test 2: Object property fuzzing (since most accept a params object)
const baseParams = {};
for (const input of BIG_NAUGHTY_LIST) {
// Try to inject the naughty value into common keys
const keys = ['pubkey', 'created_at', 'content', 'tags', 'additionalTags', 'metadata', 'relays'];
for (const key of keys) {
try {
func({ ...baseParams, [key]: input });
} catch (error) {
logFailure(funcName, [{ [key]: input }], error);
}
}
}
}
// 5000 iterations to get good coverage
runFuzzer("nostrEventSchemas", fuzzNostrEventSchemas, 5000);
const targets = {
buildVideoPostEvent,
buildVideoMirrorEvent,
buildRepostEvent,
buildShareEvent,
buildRelayListEvent,
buildDmRelayListEvent,
buildProfileMetadataEvent,
buildMuteListEvent,
buildDeletionEvent,
buildLegacyDirectMessageEvent,
buildDmAttachmentEvent,
buildDmReadReceiptEvent,
buildDmTypingIndicatorEvent,
buildViewEvent,
buildZapRequestEvent,
buildReactionEvent,
buildCommentEvent,
buildWatchHistoryEvent,
buildSubscriptionListEvent,
buildBlockListEvent,
buildHashtagPreferenceEvent,
sanitizeAdditionalTags,
};
for (const [name, func] of Object.entries(targets)) {
fuzzFunction(func, name);
}
fs.writeFileSync(REPORT_FILE, JSON.stringify(report, null, 2));
console.log(`Fuzzing complete. Report saved to ${REPORT_FILE}`);