From 932e57c72464f81ffb54578b10d5c660189cd0a4 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:12:42 +0000 Subject: [PATCH] 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 <53631862+PR0M3TH3AN@users.noreply.github.com> --- .../issue-1769184336556-zi9tj5l6/README.md | 13 + .../issue-1769184336556-zi9tj5l6/repro.mjs | 17 + .../issue-1769184336558-dkgdojxe/README.md | 12 + .../issue-1769184336558-dkgdojxe/repro.mjs | 17 + .../issue-1769184336717-vkjo0p01/README.md | 13 + .../issue-1769184336717-vkjo0p01/repro.mjs | 17 + .../issue-1769184336720-gw7etqt5/README.md | 12 + .../issue-1769184336720-gw7etqt5/repro.mjs | 17 + .../issue-1769184336734-6hvf78rb/README.md | 13 + .../issue-1769184336734-6hvf78rb/repro.mjs | 17 + .../issue-1769184336735-ylfpm4ul/README.md | 12 + .../issue-1769184336735-ylfpm4ul/repro.mjs | 17 + .../issue-1769184336740-2j4kylui/README.md | 13 + .../issue-1769184336740-2j4kylui/repro.mjs | 17 + .../issue-1769184336741-uvul9skf/README.md | 12 + .../issue-1769184336741-uvul9skf/repro.mjs | 17 + .../issue-1769184336748-2cyuyxol/README.md | 13 + .../issue-1769184336748-2cyuyxol/repro.mjs | 17 + .../issue-1769184336749-04htj2e5/README.md | 12 + .../issue-1769184336749-04htj2e5/repro.mjs | 17 + .../issue-1769184354751-yhtkxzw3/README.md | 14 + .../issue-1769184354751-yhtkxzw3/repro.mjs | 16 + js/magnetUtils.js | 8 +- js/nostrEventSchemas.js | 24 +- scripts/agent/fuzz-dmDecryptor.mjs | 283 +++++++-- scripts/agent/fuzz-magnetUtils.mjs | 232 ++++++-- scripts/agent/fuzz-nostrEventSchemas.mjs | 550 ++++++++---------- 27 files changed, 1012 insertions(+), 410 deletions(-) create mode 100644 examples/reproducers/issue-1769184336556-zi9tj5l6/README.md create mode 100644 examples/reproducers/issue-1769184336556-zi9tj5l6/repro.mjs create mode 100644 examples/reproducers/issue-1769184336558-dkgdojxe/README.md create mode 100644 examples/reproducers/issue-1769184336558-dkgdojxe/repro.mjs create mode 100644 examples/reproducers/issue-1769184336717-vkjo0p01/README.md create mode 100644 examples/reproducers/issue-1769184336717-vkjo0p01/repro.mjs create mode 100644 examples/reproducers/issue-1769184336720-gw7etqt5/README.md create mode 100644 examples/reproducers/issue-1769184336720-gw7etqt5/repro.mjs create mode 100644 examples/reproducers/issue-1769184336734-6hvf78rb/README.md create mode 100644 examples/reproducers/issue-1769184336734-6hvf78rb/repro.mjs create mode 100644 examples/reproducers/issue-1769184336735-ylfpm4ul/README.md create mode 100644 examples/reproducers/issue-1769184336735-ylfpm4ul/repro.mjs create mode 100644 examples/reproducers/issue-1769184336740-2j4kylui/README.md create mode 100644 examples/reproducers/issue-1769184336740-2j4kylui/repro.mjs create mode 100644 examples/reproducers/issue-1769184336741-uvul9skf/README.md create mode 100644 examples/reproducers/issue-1769184336741-uvul9skf/repro.mjs create mode 100644 examples/reproducers/issue-1769184336748-2cyuyxol/README.md create mode 100644 examples/reproducers/issue-1769184336748-2cyuyxol/repro.mjs create mode 100644 examples/reproducers/issue-1769184336749-04htj2e5/README.md create mode 100644 examples/reproducers/issue-1769184336749-04htj2e5/repro.mjs create mode 100644 examples/reproducers/issue-1769184354751-yhtkxzw3/README.md create mode 100644 examples/reproducers/issue-1769184354751-yhtkxzw3/repro.mjs diff --git a/examples/reproducers/issue-1769184336556-zi9tj5l6/README.md b/examples/reproducers/issue-1769184336556-zi9tj5l6/README.md new file mode 100644 index 00000000..91dda7dd --- /dev/null +++ b/examples/reproducers/issue-1769184336556-zi9tj5l6/README.md @@ -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 () + 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 \ No newline at end of file diff --git a/examples/reproducers/issue-1769184336556-zi9tj5l6/repro.mjs b/examples/reproducers/issue-1769184336556-zi9tj5l6/repro.mjs new file mode 100644 index 00000000..338f598c --- /dev/null +++ b/examples/reproducers/issue-1769184336556-zi9tj5l6/repro.mjs @@ -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); +} diff --git a/examples/reproducers/issue-1769184336558-dkgdojxe/README.md b/examples/reproducers/issue-1769184336558-dkgdojxe/README.md new file mode 100644 index 00000000..c1b776e2 --- /dev/null +++ b/examples/reproducers/issue-1769184336558-dkgdojxe/README.md @@ -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 () + 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 \ No newline at end of file diff --git a/examples/reproducers/issue-1769184336558-dkgdojxe/repro.mjs b/examples/reproducers/issue-1769184336558-dkgdojxe/repro.mjs new file mode 100644 index 00000000..338f598c --- /dev/null +++ b/examples/reproducers/issue-1769184336558-dkgdojxe/repro.mjs @@ -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); +} diff --git a/examples/reproducers/issue-1769184336717-vkjo0p01/README.md b/examples/reproducers/issue-1769184336717-vkjo0p01/README.md new file mode 100644 index 00000000..2407ff79 --- /dev/null +++ b/examples/reproducers/issue-1769184336717-vkjo0p01/README.md @@ -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 () + 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 \ No newline at end of file diff --git a/examples/reproducers/issue-1769184336717-vkjo0p01/repro.mjs b/examples/reproducers/issue-1769184336717-vkjo0p01/repro.mjs new file mode 100644 index 00000000..2ac0cd74 --- /dev/null +++ b/examples/reproducers/issue-1769184336717-vkjo0p01/repro.mjs @@ -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); +} diff --git a/examples/reproducers/issue-1769184336720-gw7etqt5/README.md b/examples/reproducers/issue-1769184336720-gw7etqt5/README.md new file mode 100644 index 00000000..6f7b7b28 --- /dev/null +++ b/examples/reproducers/issue-1769184336720-gw7etqt5/README.md @@ -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 () + 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 \ No newline at end of file diff --git a/examples/reproducers/issue-1769184336720-gw7etqt5/repro.mjs b/examples/reproducers/issue-1769184336720-gw7etqt5/repro.mjs new file mode 100644 index 00000000..2ac0cd74 --- /dev/null +++ b/examples/reproducers/issue-1769184336720-gw7etqt5/repro.mjs @@ -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); +} diff --git a/examples/reproducers/issue-1769184336734-6hvf78rb/README.md b/examples/reproducers/issue-1769184336734-6hvf78rb/README.md new file mode 100644 index 00000000..5cc9ac6f --- /dev/null +++ b/examples/reproducers/issue-1769184336734-6hvf78rb/README.md @@ -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 () + 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 \ No newline at end of file diff --git a/examples/reproducers/issue-1769184336734-6hvf78rb/repro.mjs b/examples/reproducers/issue-1769184336734-6hvf78rb/repro.mjs new file mode 100644 index 00000000..f31f31de --- /dev/null +++ b/examples/reproducers/issue-1769184336734-6hvf78rb/repro.mjs @@ -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); +} diff --git a/examples/reproducers/issue-1769184336735-ylfpm4ul/README.md b/examples/reproducers/issue-1769184336735-ylfpm4ul/README.md new file mode 100644 index 00000000..925e681e --- /dev/null +++ b/examples/reproducers/issue-1769184336735-ylfpm4ul/README.md @@ -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 () + 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 \ No newline at end of file diff --git a/examples/reproducers/issue-1769184336735-ylfpm4ul/repro.mjs b/examples/reproducers/issue-1769184336735-ylfpm4ul/repro.mjs new file mode 100644 index 00000000..f31f31de --- /dev/null +++ b/examples/reproducers/issue-1769184336735-ylfpm4ul/repro.mjs @@ -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); +} diff --git a/examples/reproducers/issue-1769184336740-2j4kylui/README.md b/examples/reproducers/issue-1769184336740-2j4kylui/README.md new file mode 100644 index 00000000..00620ba0 --- /dev/null +++ b/examples/reproducers/issue-1769184336740-2j4kylui/README.md @@ -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 () + 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 \ No newline at end of file diff --git a/examples/reproducers/issue-1769184336740-2j4kylui/repro.mjs b/examples/reproducers/issue-1769184336740-2j4kylui/repro.mjs new file mode 100644 index 00000000..bde0aa95 --- /dev/null +++ b/examples/reproducers/issue-1769184336740-2j4kylui/repro.mjs @@ -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); +} diff --git a/examples/reproducers/issue-1769184336741-uvul9skf/README.md b/examples/reproducers/issue-1769184336741-uvul9skf/README.md new file mode 100644 index 00000000..10cd8f19 --- /dev/null +++ b/examples/reproducers/issue-1769184336741-uvul9skf/README.md @@ -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 () + 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 \ No newline at end of file diff --git a/examples/reproducers/issue-1769184336741-uvul9skf/repro.mjs b/examples/reproducers/issue-1769184336741-uvul9skf/repro.mjs new file mode 100644 index 00000000..bde0aa95 --- /dev/null +++ b/examples/reproducers/issue-1769184336741-uvul9skf/repro.mjs @@ -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); +} diff --git a/examples/reproducers/issue-1769184336748-2cyuyxol/README.md b/examples/reproducers/issue-1769184336748-2cyuyxol/README.md new file mode 100644 index 00000000..0ec5ab40 --- /dev/null +++ b/examples/reproducers/issue-1769184336748-2cyuyxol/README.md @@ -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 () + 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 \ No newline at end of file diff --git a/examples/reproducers/issue-1769184336748-2cyuyxol/repro.mjs b/examples/reproducers/issue-1769184336748-2cyuyxol/repro.mjs new file mode 100644 index 00000000..a71a04fc --- /dev/null +++ b/examples/reproducers/issue-1769184336748-2cyuyxol/repro.mjs @@ -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); +} diff --git a/examples/reproducers/issue-1769184336749-04htj2e5/README.md b/examples/reproducers/issue-1769184336749-04htj2e5/README.md new file mode 100644 index 00000000..88b38ec3 --- /dev/null +++ b/examples/reproducers/issue-1769184336749-04htj2e5/README.md @@ -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 () + 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 \ No newline at end of file diff --git a/examples/reproducers/issue-1769184336749-04htj2e5/repro.mjs b/examples/reproducers/issue-1769184336749-04htj2e5/repro.mjs new file mode 100644 index 00000000..a71a04fc --- /dev/null +++ b/examples/reproducers/issue-1769184336749-04htj2e5/repro.mjs @@ -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); +} diff --git a/examples/reproducers/issue-1769184354751-yhtkxzw3/README.md b/examples/reproducers/issue-1769184354751-yhtkxzw3/README.md new file mode 100644 index 00000000..70b9cb5e --- /dev/null +++ b/examples/reproducers/issue-1769184354751-yhtkxzw3/README.md @@ -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) \ No newline at end of file diff --git a/examples/reproducers/issue-1769184354751-yhtkxzw3/repro.mjs b/examples/reproducers/issue-1769184354751-yhtkxzw3/repro.mjs new file mode 100644 index 00000000..41a1a4fc --- /dev/null +++ b/examples/reproducers/issue-1769184354751-yhtkxzw3/repro.mjs @@ -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); +} diff --git a/js/magnetUtils.js b/js/magnetUtils.js index af06ab63..6e547fe4 100644 --- a/js/magnetUtils.js +++ b/js/magnetUtils.js @@ -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 { diff --git a/js/nostrEventSchemas.js b/js/nostrEventSchemas.js index 7d751998..c6c3834e 100644 --- a/js/nostrEventSchemas.js +++ b/js/nostrEventSchemas.js @@ -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) { diff --git a/scripts/agent/fuzz-dmDecryptor.mjs b/scripts/agent/fuzz-dmDecryptor.mjs index 14913655..ee74ea0a 100644 --- a/scripts/agent/fuzz-dmDecryptor.mjs +++ b/scripts/agent/fuzz-dmDecryptor.mjs @@ -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}`); diff --git a/scripts/agent/fuzz-magnetUtils.mjs b/scripts/agent/fuzz-magnetUtils.mjs index 1cc19a7e..ffe06e73 100644 --- a/scripts/agent/fuzz-magnetUtils.mjs +++ b/scripts/agent/fuzz-magnetUtils.mjs @@ -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}`); diff --git a/scripts/agent/fuzz-nostrEventSchemas.mjs b/scripts/agent/fuzz-nostrEventSchemas.mjs index 72a93941..e6046ccf 100644 --- a/scripts/agent/fuzz-nostrEventSchemas.mjs +++ b/scripts/agent/fuzz-nostrEventSchemas.mjs @@ -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}`);