T-HUB/packages/safe-content/src/corpus.test.tsx
glass beadian aa5e262f3a Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives:
publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style
column wall with hostile-content-safe rendering, live control room with Auto
Director, bridged chat governance with polls and quorum, distributed sandboxed
transcode workers, encrypted content-addressed archive, monthly analytics.

MIT licensed. See README.md and docs/.
2026-10-10 11:56:42 +00:00

207 lines
9.2 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { ATTACKER_ORIGIN, FORGED_DOCUMENTS, MALICIOUS_HTML, MALICIOUS_LINK_PREVIEWS, MALICIOUS_PLAIN } from "./corpus.js";
import { parseProviderHtml } from "./html.js";
import { parsePlainText } from "./plain.js";
import { DisplayName, LinkPreviewCard, RichText } from "./react.js";
import { parseSafeHttpUrl } from "./url.js";
const PROXY_PREFIX = "/api/media-proxy?url=";
const ALLOWED_TAGS = new Set(["div", "p", "a", "span", "br", "img", "strong", "bdi"]);
const ALLOWED_ATTRS = new Set([
"class", "dir", "href", "rel", "target", "referrerpolicy", "src", "alt", "title",
"loading", "decoding", "draggable", "data-truncated", "data-confusable",
]);
function unescapeAttr(value: string): string {
return value.replace(/&quot;/g, '"').replace(/&#x27;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
}
/**
* Asserts rendered markup is inert: only allowlisted tags/attributes, every
* href an absolute credential-free http(s) URL with the safe rel, every src
* routed through the media proxy (so nothing is fetched from an attacker
* origin), and no text that the browser could re-parse as markup.
*/
function assertInert(html: string): void {
const tagRe = /<(\/?)([a-zA-Z][a-zA-Z0-9-]*)((?:\s+[a-zA-Z-]+(?:="[^"]*")?)*)\s*\/?>/g;
const stripped = html.replace(tagRe, "");
// Anything left that looks like a tag would mean unescaped markup leaked out.
expect(stripped).not.toMatch(/<[a-zA-Z!/?]/);
for (const match of html.matchAll(tagRe)) {
const [, closing, tag, attrs = ""] = match;
if (closing) {
expect(attrs.trim()).toBe("");
continue;
}
expect(ALLOWED_TAGS, `tag <${tag}> in ${html}`).toContain(tag!.toLowerCase());
const attrMap = new Map<string, string>();
for (const attr of attrs.matchAll(/([a-zA-Z-]+)(?:="([^"]*)")?/g)) {
const name = attr[1]!.toLowerCase();
const value = unescapeAttr(attr[2] ?? "");
expect(ALLOWED_ATTRS, `attribute ${name} in ${html}`).toContain(name);
expect(name).not.toMatch(/^on/);
attrMap.set(name, value);
if (name === "href") {
expect(value).toMatch(/^https?:\/\/[^/]/);
expect(parseSafeHttpUrl(value)?.href).toBe(value);
}
if (name === "src") {
expect(value.startsWith(PROXY_PREFIX), `src ${value}`).toBe(true);
expect(value).not.toMatch(/^\/\//);
}
if (name !== "href" && name !== "src") expect(value).not.toMatch(/(javascript|vbscript|data):/i);
}
if (tag === "a") {
expect(attrMap.get("rel")).toBe("noopener noreferrer nofollow ugc");
expect(attrMap.get("referrerpolicy")).toBe("no-referrer");
}
if (tag === "img") expect(attrMap.get("referrerpolicy")).toBe("no-referrer");
}
expect(html).not.toMatch(/dangerouslySetInnerHTML|srcset|srcdoc|style=/i);
}
/** Every request-causing attribute (src) targets our own origin only. */
function assertNoAttackerRequests(html: string): void {
for (const m of html.matchAll(/\ssrc="([^"]*)"/g)) {
expect(m[1]!.startsWith(PROXY_PREFIX)).toBe(true);
}
expect(html).not.toMatch(new RegExp(`src="https?://[^"]*${ATTACKER_ORIGIN.replace(".", "\\.")}`));
}
const emojis = [
{ shortcode: "blobcat", url: "https://cdn.ok.example/blobcat.png" },
{ shortcode: "evil", url: "javascript:alert(1)" },
{ shortcode: "x\" onerror=\"alert(1)", url: "https://cdn.ok.example/x.png" },
];
describe("malicious HTML corpus", () => {
it.each(MALICIOUS_HTML.map((p) => [p]))("renders inert: %s", (payload) => {
const doc = parseProviderHtml(payload, { emojis });
const html = renderToStaticMarkup(<RichText doc={doc} />);
assertInert(html);
assertNoAttackerRequests(html);
expect(JSON.stringify(doc)).not.toMatch(/javascript:|vbscript:|"data:/i);
});
it("drops script/style/iframe content rather than showing it", () => {
const html = renderToStaticMarkup(
<RichText doc={parseProviderHtml("<p>a<script>alert(1)</script>b<style>x{}</style>c<iframe>d</iframe>e<svg><text>f</text></svg>g</p>")} />,
);
expect(html).toBe('<div dir="auto"><p dir="auto">abceg</p></div>');
});
it("renders unsafe-scheme links as non-clickable text", () => {
for (const href of ["javascript:alert(1)", "data:text/html,x", "vbscript:x", "//evil.example/", "https://u:p@evil.example/", "file:///etc/passwd"]) {
const html = renderToStaticMarkup(<RichText doc={parseProviderHtml(`<a href="${href}">label</a>`)} />);
expect(html).toBe('<div dir="auto"><p dir="auto">label</p></div>');
}
});
it("shows the real destination host when the label names another host", () => {
const html = renderToStaticMarkup(<RichText doc={parseProviderHtml('<a href="https://evil.example/login">https://bank.example/login</a>')} />);
expect(html).toContain('<span class="sc-link-host"> (evil.example)</span>');
});
it("exposes homoglyph IDN hosts as punycode", () => {
const doc = parseProviderHtml('<a href="https://pаypal.com/">paypal.com</a>');
const link = doc.children[0]!.children[0]!;
expect(link).toMatchObject({ type: "link", host: "xn--pypal-4ve.com" });
expect(renderToStaticMarkup(<RichText doc={doc} />)).toContain("(xn--pypal-4ve.com)");
});
it("strips bidi overrides and isolates from body text", () => {
const doc = parseProviderHtml("<p>file‮gnp.exe ⁦x⁩</p>");
expect(JSON.stringify(doc)).not.toMatch(/[‪-‮⁦-⁩]/);
});
it("emits entity-encoded markup only as escaped text", () => {
const html = renderToStaticMarkup(<RichText doc={parseProviderHtml("&lt;script&gt;alert(1)&lt;/script&gt;")} />);
expect(html).toContain("&lt;script&gt;alert(1)&lt;/script&gt;");
});
});
describe("malicious plain-text corpus", () => {
it.each(MALICIOUS_PLAIN.map((p) => [p]))("renders inert: %s", (payload) => {
const html = renderToStaticMarkup(<RichText doc={parsePlainText(payload, { emojis })} />);
assertInert(html);
assertNoAttackerRequests(html);
});
it("never links javascript:/data: or protocol-relative text", () => {
const html = renderToStaticMarkup(<RichText doc={parsePlainText("javascript:alert(1) data:text/html,x //evil.example/x")} />);
expect(html).not.toContain("<a");
});
});
describe("link-preview payloads", () => {
it.each(MALICIOUS_LINK_PREVIEWS.map((p, i) => [i, p]))("renders inert: #%s", (_i, preview) => {
const html = renderToStaticMarkup(<LinkPreviewCard preview={preview} />);
assertInert(html);
assertNoAttackerRequests(html);
});
it("rejects unsafe targets entirely", () => {
expect(renderToStaticMarkup(<LinkPreviewCard preview={{ url: "javascript:alert(1)", title: "x" }} />)).toBe("");
});
it("routes preview images through the media proxy", () => {
const html = renderToStaticMarkup(<LinkPreviewCard preview={{ url: "https://ok.example/", image: "https://evil.example/og.png" }} />);
expect(html).toContain(`src="${PROXY_PREFIX}https%3A%2F%2Fevil.example%2Fog.png"`);
});
it("caps preview text length", () => {
const html = renderToStaticMarkup(<LinkPreviewCard preview={{ url: "https://ok.example/", title: "x".repeat(100_000) }} />);
expect(html.length).toBeLessThan(2_000);
});
});
describe("forged ASTs", () => {
it.each(FORGED_DOCUMENTS.map((d, i) => [i, d]))("are re-validated at render: #%s", (_i, doc) => {
const html = renderToStaticMarkup(<RichText doc={doc} />);
assertInert(html);
assertNoAttackerRequests(html);
expect(html).not.toMatch(/<[^>]*onerror/i);
});
});
describe("display names", () => {
it("removes bidi and zero-width spoofing characters", () => {
const html = renderToStaticMarkup(<DisplayName name={"ad​min‮nimda <script>"} />);
expect(html).toBe("<bdi>adminnimda &lt;script&gt;</bdi>");
});
it("flags mixed-script homoglyph names", () => {
expect(renderToStaticMarkup(<DisplayName name={"pаypal"} />)).toContain('data-confusable="true"');
expect(renderToStaticMarkup(<DisplayName name="paypal" />)).not.toContain("data-confusable");
});
});
describe("huge and deeply nested input", () => {
const cases: [string, string][] = [
["1MB of '<'", "<".repeat(1_000_000)],
["unterminated attribute", "<a href=\"" + "x".repeat(1_000_000)],
["100k nested divs", "<div>".repeat(100_000) + "deep" + "</div>".repeat(100_000)],
["100k nested svg", "<svg>".repeat(100_000) + "<img src=x onerror=alert(1)>"],
["50k links", '<a href="https://ok.example/">x</a>'.repeat(50_000)],
["comment bomb", "<!--".repeat(200_000)],
["attribute flood", "<a " + "x=1 ".repeat(200_000) + 'href="https://ok.example/">x</a>'],
["unclosed raw text", "<script>" + "a".repeat(1_000_000)],
];
it.each(cases)("%s: bounded time and output, inert", (_name, payload) => {
const start = performance.now();
const doc = parseProviderHtml(payload);
const html = renderToStaticMarkup(<RichText doc={doc} />);
expect(performance.now() - start).toBeLessThan(2_000);
expect(html.length).toBeLessThan(200_000);
assertInert(html);
expect(doc.truncated).toBe(true);
});
it("plain text: huge input is clipped", () => {
const doc = parsePlainText("https://ok.example/ @a #b\n".repeat(200_000));
expect(doc.truncated).toBe(true);
assertInert(renderToStaticMarkup(<RichText doc={doc} />));
});
});