// SPDX-License-Identifier: 0BSD /** * Exploratory fuzzing and light oracles for Micron/Nomad/codec2/memory hardening. * * Light oracles = invariant checks on every output (no full differential engine): * - no script / javascript / iframe / fixed overlays after sanitize * - codec2 size gate rejects oversize and accepts undersize * - memory warning classifier ignores non-memory kinds */ import { beforeEach, describe, expect, it, vi } from "vitest"; import MicronParser from "@/js/MicronParser.js"; import { renderNomadHtmlPage, renderNomadMarkdown, sanitizeNomadHtmlFragment, stripExternalFromCss, stripOverlayFromCss, } from "@/js/NomadPageRenderer.js"; import { MAX_CODEC2_DECODED_RAW_BYTES, MAX_CODEC2_ENCODED_BYTES, assertByteLengthAtMost, } from "@/js/codec2DecodeLimits.js"; import { evaluateClientHeapSample, handleHealthWarningPayload, isMemoryHealthWarningPayload, resetMemoryWarningStateForTests, } from "@/js/healthMemoryWarning.js"; const DANGEROUS_TAGS = [/]/i, /]/i, /]/i, /]/i]; const OVERLAY_STYLE_ATTACKS = [ 'style="position:fixed !important; inset:0; z-index:99999"', 'style="position/**/:fixed; top:0; left:0"', 'style="position: sticky !important; transform: translateY(0)"', 'style="POSITION:FiXeD; width:100vw; height:100vh"', `style="position:fi${String.fromCharCode(0x200b)}xed; inset:0"`, `style="position:fi${String.fromCharCode(0x00ad)}xed"`, ]; function assertSafeHtmlOracle(html) { expect(typeof html).toBe("string"); const lower = html.toLowerCase(); for (const re of DANGEROUS_TAGS) { expect(html).not.toMatch(re); } // Executable URL contexts only (plain text mentioning javascript: is fine). expect(lower).not.toMatch(/\bhref\s*=\s*["']?\s*javascript\s*:/); expect(lower).not.toMatch(/\bsrc\s*=\s*["']?\s*javascript\s*:/); expect(lower).not.toMatch(/\bhref\s*=\s*["']?\s*vbscript\s*:/); expect(lower).not.toMatch(/position\s*:\s*fixed/); expect(lower).not.toMatch(/position\s*:\s*sticky/); expect(lower).not.toMatch(/position\s*:\s*absolute/); } function randomStyleValue(len) { const tokens = [ "position:fixed", "position:fixed !important", "position/**/:fixed", "position:sticky", "inset:0", "z-index:99999", "top:0", "left:0", "transform:scale(1)", "width:100vw", "height:100vh", "color:red", "font-size:12px", "background:#fff", "/*comment*/", "", ]; const parts = []; for (let i = 0; i < len; i++) { parts.push(tokens[Math.floor(Math.random() * tokens.length)]); } return parts.join("; "); } function randomHtmlSnippet() { const snippets = [ "", "", "x", "", "", "", '
phish
', "

x

", "normal text " + String.fromCharCode(0x1f4a3), "x", ]; return snippets[Math.floor(Math.random() * snippets.length)]; } describe("exploratory + oracle-light: Micron overlay strip", () => { it("oracle: known overlay attacks lose fixed/sticky", () => { for (const attack of OVERLAY_STYLE_ATTACKS) { const html = MicronParser.stripOverlayStyles(`
x
`); assertSafeHtmlOracle(html); expect(html).toContain("x"); } }); it("exploratory: random style attributes never throw and never keep fixed/sticky", () => { for (let i = 0; i < 300; i++) { const style = randomStyleValue(1 + (i % 8)); let out; expect(() => { out = MicronParser.stripOverlayStyles(`n`); }).not.toThrow(); assertSafeHtmlOracle(out); } }); it("exploratory: sanitizeRenderedMicronHtml on mixed XSS payloads", () => { for (let i = 0; i < 200; i++) { const payload = randomHtmlSnippet() + (i % 3 === 0 ? `
z
` : ""); let out; expect(() => { out = MicronParser.sanitizeRenderedMicronHtml(payload); }).not.toThrow(); assertSafeHtmlOracle(out); } }); }); describe("exploratory + oracle-light: Nomad HTML/MD/CSS", () => { it("oracle: style-block overlays are neutralized", () => { const css = stripOverlayFromCss(".x{position:fixed !important;inset:0;z-index:9}"); expect(css.toLowerCase()).not.toMatch(/position\s*:\s*fixed/); expect(stripExternalFromCss("@import url(http://x); .a{position:sticky}")).not.toMatch(/@import/i); }); it("exploratory: renderNomadHtmlPage random payloads stay within oracle", () => { for (let i = 0; i < 250; i++) { let s = randomHtmlSnippet(); const extra = Math.floor(Math.random() * 400); for (let j = 0; j < extra; j++) { s += String.fromCharCode(Math.floor(Math.random() * 128)); } let out; expect(() => { out = renderNomadHtmlPage(`${s}`); }).not.toThrow(); assertSafeHtmlOracle(out); } }); it("exploratory: markdown + fragment path respect oracle", () => { for (let i = 0; i < 120; i++) { const md = `${"#".repeat(1 + (i % 3))} title ${i}\n\n[x](javascript:alert(1))\n\n
y
`; let html; let frag; expect(() => { html = renderNomadMarkdown(md); frag = sanitizeNomadHtmlFragment(`
${randomHtmlSnippet()}
`); }).not.toThrow(); assertSafeHtmlOracle(html); assertSafeHtmlOracle(frag); } }); }); describe("exploratory + oracle-light: codec2 size gate", () => { it("oracle: under-cap buffers pass, over-cap buffers throw", () => { expect(assertByteLengthAtMost(new Uint8Array(0), MAX_CODEC2_ENCODED_BYTES).byteLength).toBe(0); expect(assertByteLengthAtMost(new Uint8Array(16), MAX_CODEC2_ENCODED_BYTES).byteLength).toBe(16); expect(() => assertByteLengthAtMost(new Uint8Array(MAX_CODEC2_ENCODED_BYTES + 1), MAX_CODEC2_ENCODED_BYTES) ).toThrow(/exceeds size limit/); expect(() => assertByteLengthAtMost(new Uint8Array(MAX_CODEC2_DECODED_RAW_BYTES + 1), MAX_CODEC2_DECODED_RAW_BYTES) ).toThrow(/exceeds size limit/); }); it("exploratory: random sizes near boundaries behave as oracle predicts", () => { const cases = [ 0, 1, MAX_CODEC2_ENCODED_BYTES - 1, MAX_CODEC2_ENCODED_BYTES, MAX_CODEC2_ENCODED_BYTES + 1, Math.floor(Math.random() * MAX_CODEC2_ENCODED_BYTES), ]; for (const n of cases) { const buf = new Uint8Array(Math.max(0, n)); if (n <= MAX_CODEC2_ENCODED_BYTES) { expect(assertByteLengthAtMost(buf, MAX_CODEC2_ENCODED_BYTES).byteLength).toBe(n); } else { expect(() => assertByteLengthAtMost(buf, MAX_CODEC2_ENCODED_BYTES)).toThrow(/exceeds size limit/); } } }); }); describe("exploratory + oracle-light: memory warning classifier", () => { beforeEach(() => { resetMemoryWarningStateForTests(); }); it("oracle: only memory_low shows toast; other kinds ignored", () => { const toast = { warning: vi.fn() }; const ignored = [ null, undefined, "memory_low", { kind: "entropy_climbing" }, { kind: "error_rate_high" }, { type: "health_warning", data: { kind: "entropy_climbing" } }, { data: { kind: "something_else" } }, ]; for (const payload of ignored) { expect(isMemoryHealthWarningPayload(payload)).toBe(false); expect(handleHealthWarningPayload(payload, toast)).toBe("ignored"); } expect(toast.warning).not.toHaveBeenCalled(); expect(handleHealthWarningPayload({ data: { kind: "memory_low" } }, toast)).toBe("shown"); expect(toast.warning).toHaveBeenCalledTimes(1); }); it("exploratory: random heap samples never throw; single spike is not a warn", () => { for (let i = 0; i < 200; i++) { const limit = 1000 + Math.floor(Math.random() * 9000); const used = Math.floor(Math.random() * limit * 1.2); let result; expect(() => { result = evaluateClientHeapSample({ jsHeapSizeLimit: limit, usedJSHeapSize: used, }); }).not.toThrow(); expect(typeof result.shouldWarn).toBe("boolean"); expect(typeof result.reason).toBe("string"); } resetMemoryWarningStateForTests(); const oneSpike = evaluateClientHeapSample({ jsHeapSizeLimit: 1000, usedJSHeapSize: 900, }); expect(oneSpike.shouldWarn).toBe(false); expect(oneSpike.reason).toBe("need_consecutive"); }); });