mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Add fuzz robustness tests for nostr schemas, dm decryptor, and magnet utils
- Created `scripts/agent/fuzz-utils.mjs` with shared random data generators. - Created `scripts/agent/fuzz-nostrEventSchemas.mjs` to fuzz event builders and validators, including `sanitizeAdditionalTags`. - Created `scripts/agent/fuzz-dmDecryptor.mjs` to fuzz DM decryption with mocked decryptors and custom reproducer generation. - Created `scripts/agent/fuzz-magnetUtils.mjs` to fuzz magnet URI normalization. - Verified that all fuzz targets run without crashes. - Saved execution reports to `artifacts/`.
This commit is contained in:
1 parent
f40b067d86
commit
f2d3650873
4 files changed
+477
-695
No files matched your search
+161
-218
@@ -1,236 +1,179 @@
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
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';
|
||||
|
||||
// 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 ITERATIONS = 1000;
|
||||
const FINDINGS = [];
|
||||
|
||||
if (!fs.existsSync(ARTIFACTS_DIR)) fs.mkdirSync(ARTIFACTS_DIR, { recursive: true });
|
||||
if (!fs.existsSync(REPRODUCERS_DIR)) fs.mkdirSync(REPRODUCERS_DIR, { recursive: true });
|
||||
|
||||
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
|
||||
function saveReproducerDM(id, input, error) {
|
||||
const reproDir = path.join('examples', 'reproducers', `fuzz-dmDecryptor-${id}`);
|
||||
if (!fs.existsSync(reproDir)) {
|
||||
fs.mkdirSync(reproDir, { recursive: true });
|
||||
}
|
||||
};
|
||||
|
||||
const reproScript = `
|
||||
import { ${funcName} } from '../../../js/dmDecryptor.js';
|
||||
const readmeContent = `# Fuzz Reproducer: dmDecryptor - ${id}
|
||||
|
||||
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);
|
||||
}
|
||||
## Error
|
||||
\`\`\`
|
||||
${error.stack || error.message}
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
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}`);
|
||||
fs.writeFileSync(path.join(reproDir, 'README.md'), readmeContent);
|
||||
|
||||
report.failures.push({
|
||||
function: funcName,
|
||||
args: args.map(a => {
|
||||
try {
|
||||
return JSON.stringify(a);
|
||||
} catch {
|
||||
return '[Circular/Unserializable]';
|
||||
// Serialize input parts
|
||||
const event = input.event;
|
||||
const context = input.context;
|
||||
|
||||
// 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
|
||||
};
|
||||
|
||||
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 scriptContent = `
|
||||
import { decryptDM } from '../../../../js/dmDecryptor.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const event = JSON.parse(fs.readFileSync('input-event.json', 'utf8'));
|
||||
const contextConfig = JSON.parse(fs.readFileSync('input-context-config.json', 'utf8'));
|
||||
|
||||
// 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";
|
||||
}
|
||||
}),
|
||||
error: {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
},
|
||||
reproDir,
|
||||
}))
|
||||
: 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}`);
|
||||
}
|
||||
|
||||
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
|
||||
});
|
||||
console.error(`FAILURE in ${funcName}: ${error.message}`);
|
||||
saveReproducerDM(id, input, error);
|
||||
}
|
||||
|
||||
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 });
|
||||
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()
|
||||
};
|
||||
|
||||
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();
|
||||
function generateRandomEvent() {
|
||||
const kind = pickOne([4, 1059, fuzzInt(0, 20000)]);
|
||||
const content = fuzzBoolean() ? fuzzString(100) : fuzzJSON(2);
|
||||
|
||||
fs.writeFileSync(REPORT_FILE, JSON.stringify(report, null, 2));
|
||||
console.log(`Fuzzing complete. Report saved to ${REPORT_FILE}`);
|
||||
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,209 +1,90 @@
|
||||
import { normalizeAndAugmentMagnet } from '../../js/magnetUtils.js';
|
||||
import {
|
||||
fuzzBoolean,
|
||||
fuzzInt,
|
||||
fuzzString,
|
||||
fuzzHexString,
|
||||
fuzzJSON,
|
||||
pickOne,
|
||||
fuzzSurrogatePairString,
|
||||
saveFuzzReport,
|
||||
saveReproducer
|
||||
} from './fuzz-utils.mjs';
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { normalizeAndAugmentMagnet, safeDecodeMagnet } from '../../js/magnetUtils.js';
|
||||
const ITERATIONS = 1000;
|
||||
const FINDINGS = [];
|
||||
|
||||
// Setup artifacts and reproducers
|
||||
const ARTIFACTS_DIR = 'artifacts';
|
||||
const REPRODUCERS_DIR = 'examples/reproducers';
|
||||
const REPORT_FILE = path.join(ARTIFACTS_DIR, 'fuzz-report-magnetUtils.json');
|
||||
|
||||
if (!fs.existsSync(ARTIFACTS_DIR)) fs.mkdirSync(ARTIFACTS_DIR, { recursive: true });
|
||||
if (!fs.existsSync(REPRODUCERS_DIR)) fs.mkdirSync(REPRODUCERS_DIR, { recursive: true });
|
||||
|
||||
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 '{}';
|
||||
}
|
||||
};
|
||||
|
||||
const reproScript = `
|
||||
import { ${funcName} } from '../../../js/magnetUtils.js';
|
||||
|
||||
const args = ${serialize(args)};
|
||||
|
||||
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,
|
||||
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
|
||||
});
|
||||
console.error(`FAILURE in ${funcName}: ${error.message}`);
|
||||
saveReproducer(target, id, input, error);
|
||||
}
|
||||
|
||||
function fuzzNormalize() {
|
||||
console.log(`Fuzzing normalizeAndAugmentMagnet...`);
|
||||
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')}`;
|
||||
|
||||
// Test 1: Fuzz rawValue
|
||||
for (const input of BIG_NAUGHTY_LIST) {
|
||||
try {
|
||||
normalizeAndAugmentMagnet(input);
|
||||
} catch (error) {
|
||||
logFailure('normalizeAndAugmentMagnet', [input], error);
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
saveFuzzReport('magnetUtils', FINDINGS);
|
||||
console.log(`Fuzzing complete. Found ${FINDINGS.length} crashes.`);
|
||||
if (FINDINGS.length > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
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}`);
|
||||
fuzzMagnetUtils();
|
||||
@@ -1,8 +1,7 @@
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
buildVideoPostEvent,
|
||||
buildHttpAuthEvent,
|
||||
buildReportEvent,
|
||||
buildVideoMirrorEvent,
|
||||
buildRepostEvent,
|
||||
buildShareEvent,
|
||||
@@ -23,226 +22,152 @@ import {
|
||||
buildSubscriptionListEvent,
|
||||
buildBlockListEvent,
|
||||
buildHashtagPreferenceEvent,
|
||||
buildAdminListEvent,
|
||||
validateEventStructure,
|
||||
sanitizeAdditionalTags,
|
||||
NOTE_TYPES
|
||||
} from '../../js/nostrEventSchemas.js';
|
||||
|
||||
// Setup artifacts and reproducers
|
||||
const ARTIFACTS_DIR = 'artifacts';
|
||||
const REPRODUCERS_DIR = 'examples/reproducers';
|
||||
const REPORT_FILE = path.join(ARTIFACTS_DIR, 'fuzz-report-nostrEventSchemas.json');
|
||||
import {
|
||||
fuzzBoolean,
|
||||
fuzzInt,
|
||||
fuzzString,
|
||||
fuzzHexString,
|
||||
fuzzJSON,
|
||||
pickOne,
|
||||
fuzzSurrogatePairString,
|
||||
saveFuzzReport,
|
||||
saveReproducer
|
||||
} from './fuzz-utils.mjs';
|
||||
|
||||
if (!fs.existsSync(ARTIFACTS_DIR)) fs.mkdirSync(ARTIFACTS_DIR, { recursive: true });
|
||||
if (!fs.existsSync(REPRODUCERS_DIR)) fs.mkdirSync(REPRODUCERS_DIR, { recursive: true });
|
||||
const ITERATIONS = 1000;
|
||||
const FINDINGS = [];
|
||||
|
||||
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 },
|
||||
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 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'];
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Starting fuzzing for nostrEventSchemas.js...');
|
||||
|
||||
// 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 a recursive object
|
||||
const recursiveObj = {};
|
||||
recursiveObj.self = recursiveObj;
|
||||
BIG_NAUGHTY_LIST.push(recursiveObj);
|
||||
builders.forEach(({ name, fn }) => {
|
||||
fuzzBuilder(name, fn, () => [generateRandomParams()]);
|
||||
});
|
||||
|
||||
const report = {
|
||||
target: 'nostrEventSchemas',
|
||||
timestamp: new Date().toISOString(),
|
||||
failures: [],
|
||||
};
|
||||
// Special case for buildAdminListEvent
|
||||
fuzzBuilder('buildAdminListEvent', buildAdminListEvent, () => {
|
||||
const listKey = pickOne(['moderation', 'editors', 'whitelist', 'blacklist', fuzzString()]);
|
||||
return [listKey, generateRandomParams()];
|
||||
});
|
||||
|
||||
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 });
|
||||
// Fuzz validateEventStructure
|
||||
fuzzBuilder('validateEventStructure', validateEventStructure, () => {
|
||||
const type = pickOne([...Object.values(NOTE_TYPES), fuzzString()]);
|
||||
const event = fuzzJSON(2); // Random object/JSON
|
||||
return [type, event];
|
||||
});
|
||||
|
||||
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')};
|
||||
|
||||
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]';
|
||||
// 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));
|
||||
}
|
||||
}),
|
||||
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);
|
||||
tags.push(tag);
|
||||
} else {
|
||||
tags.push(pickOne([null, undefined, 123, {}, "string"]));
|
||||
}
|
||||
}
|
||||
return [tags];
|
||||
}
|
||||
// completely random input
|
||||
return [fuzzJSON(2)];
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
saveFuzzReport('nostrEventSchemas', FINDINGS);
|
||||
console.log(`Fuzzing complete. Found ${FINDINGS.length} crashes.`);
|
||||
if (FINDINGS.length > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
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}`);
|
||||
+101
-68
@@ -1,94 +1,127 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { randomBytes } from 'crypto';
|
||||
|
||||
export function randomString(length = 100) {
|
||||
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 fuzzBoolean() {
|
||||
return Math.random() < 0.5;
|
||||
}
|
||||
|
||||
export function randomUnicodeString(length = 100) {
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
// Random printable unicode range (roughly)
|
||||
result += String.fromCharCode(Math.floor(Math.random() * 0xFFFF));
|
||||
}
|
||||
return result;
|
||||
export function fuzzInt(min = 0, max = 100) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
export function randomSurrogateString(length = 100) {
|
||||
export function pickOne(options) {
|
||||
if (!Array.isArray(options) || options.length === 0) return undefined;
|
||||
return options[Math.floor(Math.random() * options.length)];
|
||||
}
|
||||
|
||||
export function fuzzString(length = 10) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
// Generate random surrogates
|
||||
const code = Math.random() > 0.5
|
||||
? Math.floor(0xD800 + Math.random() * (0xDBFF - 0xD800 + 1))
|
||||
: Math.floor(0xDC00 + Math.random() * (0xDFFF - 0xDC00 + 1));
|
||||
result += String.fromCharCode(code);
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function randomInt(max = 10000) {
|
||||
return Math.floor(Math.random() * max);
|
||||
export function fuzzHexString(length = 64) {
|
||||
const chars = '0123456789abcdef';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function randomBool() {
|
||||
return Math.random() > 0.5;
|
||||
export function fuzzSurrogatePairString(length = 10) {
|
||||
// Generate a string that might contain unpaired surrogates
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
if (Math.random() < 0.1) {
|
||||
// High surrogate
|
||||
result += String.fromCharCode(fuzzInt(0xD800, 0xDBFF));
|
||||
} else if (Math.random() < 0.1) {
|
||||
// Low surrogate
|
||||
result += String.fromCharCode(fuzzInt(0xDC00, 0xDFFF));
|
||||
} else {
|
||||
result += fuzzString(1);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function randomObject(depth = 2) {
|
||||
if (depth <= 0) return randomString(10);
|
||||
export function fuzzJSON(depth = 2) {
|
||||
if (depth <= 0) {
|
||||
const type = pickOne(['string', 'number', 'boolean', 'null']);
|
||||
switch (type) {
|
||||
case 'string': return fuzzString();
|
||||
case 'number': return fuzzInt();
|
||||
case 'boolean': return fuzzBoolean();
|
||||
case 'null': return null;
|
||||
}
|
||||
}
|
||||
|
||||
const type = pickOne(['object', 'array']);
|
||||
if (type === 'object') {
|
||||
const obj = {};
|
||||
const keys = randomInt(5) + 1;
|
||||
const keys = fuzzInt(1, 5);
|
||||
for (let i = 0; i < keys; i++) {
|
||||
obj[randomString(5)] = Math.random() > 0.5 ? randomString(10) : randomObject(depth - 1);
|
||||
obj[fuzzString(5)] = fuzzJSON(depth - 1);
|
||||
}
|
||||
return obj;
|
||||
} else {
|
||||
const arr = [];
|
||||
const len = fuzzInt(1, 5);
|
||||
for (let i = 0; i < len; i++) {
|
||||
arr.push(fuzzJSON(depth - 1));
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
|
||||
export function randomHex(length = 64) {
|
||||
return randomBytes(length / 2).toString('hex');
|
||||
export function saveFuzzReport(targetName, findings) {
|
||||
const reportDir = 'artifacts';
|
||||
if (!fs.existsSync(reportDir)) {
|
||||
fs.mkdirSync(reportDir, { recursive: true });
|
||||
}
|
||||
const reportPath = path.join(reportDir, `fuzz-report-${targetName}.json`);
|
||||
const report = {
|
||||
target: targetName,
|
||||
date: new Date().toISOString(),
|
||||
findings: findings
|
||||
};
|
||||
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
||||
console.log(`Fuzz report saved to ${reportPath}`);
|
||||
}
|
||||
|
||||
export function pickRandom(arr) {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
export function saveReproducer(targetName, id, input, error) {
|
||||
const reproDir = path.join('examples', 'reproducers', `fuzz-${targetName}-${id}`);
|
||||
if (!fs.existsSync(reproDir)) {
|
||||
fs.mkdirSync(reproDir, { recursive: true });
|
||||
}
|
||||
|
||||
export function fuzzGenerators() {
|
||||
const generators = [
|
||||
() => null,
|
||||
() => undefined,
|
||||
() => "",
|
||||
() => " ",
|
||||
() => 0,
|
||||
() => 1,
|
||||
() => -1,
|
||||
() => NaN,
|
||||
() => Infinity,
|
||||
() => -Infinity,
|
||||
() => [],
|
||||
() => [null],
|
||||
() => {},
|
||||
() => randomString(1000),
|
||||
() => randomUnicodeString(100),
|
||||
() => randomSurrogateString(20),
|
||||
() => randomObject(),
|
||||
() => randomHex(64),
|
||||
() => "magnet:?xt=urn:btih:" + randomHex(40),
|
||||
() => "https://" + randomString(20) + ".com",
|
||||
() => {
|
||||
const obj = {};
|
||||
obj.self = obj;
|
||||
return obj; // Circular
|
||||
}
|
||||
];
|
||||
return generators;
|
||||
}
|
||||
const readmeContent = `# Fuzz Reproducer: ${targetName} - ${id}
|
||||
|
||||
export function getRandomFuzzInput() {
|
||||
const gens = fuzzGenerators();
|
||||
return pickRandom(gens)();
|
||||
## Error
|
||||
\`\`\`
|
||||
${error.stack || error.message}
|
||||
\`\`\`
|
||||
|
||||
## Input
|
||||
See \`input.json\`
|
||||
`;
|
||||
|
||||
fs.writeFileSync(path.join(reproDir, 'README.md'), readmeContent);
|
||||
fs.writeFileSync(path.join(reproDir, 'input.json'), JSON.stringify(input, null, 2));
|
||||
|
||||
// Create a minimal reproduction script
|
||||
const scriptContent = `
|
||||
import fs from 'fs';
|
||||
// You would need to adjust imports based on the target
|
||||
// This is a placeholder template
|
||||
const input = JSON.parse(fs.readFileSync('input.json', 'utf8'));
|
||||
console.log('Running reproduction with input:', input);
|
||||
`;
|
||||
fs.writeFileSync(path.join(reproDir, 'repro.mjs'), scriptContent);
|
||||
|
||||
console.log(`Reproducer saved to ${reproDir}`);
|
||||
}
|
||||
Reference in new issue
Block a user