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("");
+ 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);
+});