diff --git a/scripts/stellar_cli_plugins.mjs b/scripts/stellar_cli_plugins.mjs index 85b717f6e..fad7400aa 100644 --- a/scripts/stellar_cli_plugins.mjs +++ b/scripts/stellar_cli_plugins.mjs @@ -1,20 +1,43 @@ import fs from "fs-extra"; import https from "https"; +import { fileURLToPath } from "url"; // In case there are plugins to exclude from the list, add them here. // E.g. "user/repo" const excludePlugins = ["haqnawaz03329-debug/haqnawaz"]; -// GitHub repo descriptions are attacker-controlled (anyone can tag a repo with -// the `stellar-cli-plugin` topic), and this content is injected into MDX, which -// renders HTML/JSX. Neutralize characters that could execute as markup/script. -function sanitize(value) { - return (value || "") - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/{/g, "{") - .replace(/}/g, "}"); +// The plugin list is built from GitHub repos that anyone can opt into by adding +// the `stellar-cli-plugin` topic, so every field below (name, description, URL) +// is attacker-controlled and ends up in an .mdx file. MDX renders Markdown, +// JSX and raw HTML, so unsanitized text allows stored XSS (e.g. a `"); + assertFullyNeutralized(out); + assert.ok(!/(^|[^\\]) { + const out = sanitizeText("danger {process.env.SECRET} here"); + assertFullyNeutralized(out); + assert.ok(!/(^|[^\\])[{}]/.test(out), "no unescaped brace remains"); +}); + +test("sanitizeText neutralizes Markdown links (phishing)", () => { + const out = sanitizeText("[Official — install now](https://evil.example.com)"); + assert.ok(!out.includes("]("), "link syntax must be broken"); + assertFullyNeutralized(out); +}); + +test("sanitizeText neutralizes Markdown images (tracking pixels)", () => { + const out = sanitizeText("![pixel](https://evil.example.com/p.png)"); + assert.ok(!out.includes("!["), "image syntax must be broken"); + assertFullyNeutralized(out); +}); + +test("sanitizeText neutralizes GFM autolinks", () => { + const out = sanitizeText("visit http://evil.example.com now"); + assert.ok(!out.includes("http://"), "bare URL scheme must be broken"); + assertFullyNeutralized(out); +}); + +test("sanitizeText collapses newlines so block constructs cannot start", () => { + const out = sanitizeText("line one\n\n### Fake heading\n> quote"); + assert.ok(!out.includes("\n"), "must be a single line"); + assertFullyNeutralized(out); +}); + +test("sanitizeText leaves plain prose untouched but escapes punctuation", () => { + // Plain words render unchanged; only punctuation gains a backslash, which the + // Markdown renderer consumes, so the visible output matches the original text. + assert.equal(sanitizeText("hello world"), "hello world"); + assert.equal(sanitizeText("a & b"), "a \\& b"); + assert.equal(sanitizeText("emphasis _here_ and *there*"), "emphasis \\_here\\_ and \\*there\\*"); +}); + +test("sanitizeText handles null/undefined", () => { + assert.equal(sanitizeText(null), ""); + assert.equal(sanitizeText(undefined), ""); +}); + +test("safeUrl accepts canonical GitHub repo URLs", () => { + assert.equal( + safeUrl("https://github.com/stellar/stellar-docs"), + "https://github.com/stellar/stellar-docs", + ); +}); + +test("safeUrl rejects javascript: scheme", () => { + assert.equal(safeUrl("javascript:alert(1)"), null); +}); + +test("safeUrl rejects data: scheme", () => { + assert.equal(safeUrl("data:text/html,"), null); +}); + +test("safeUrl rejects non-https schemes", () => { + assert.equal(safeUrl("http://github.com/a/b"), null); +}); + +test("safeUrl rejects look-alike and other hosts", () => { + assert.equal(safeUrl("https://github.com.evil.com/a/b"), null); + assert.equal(safeUrl("https://evil.com/a/b"), null); +}); + +test("safeUrl rejects malformed input", () => { + assert.equal(safeUrl("not a url"), null); + assert.equal(safeUrl(null), null); +});