Yellow Theme

Next start · October 2026

Notes / 15

2026-09-15

Our SafeMarkdown allowed //evil.example

Activity markdown was supposed to allow https links and same-origin paths only. We pasted [x](//evil.example) into a fixture. The page rendered a live anchor.

Thesis: startsWith("/") is not “same origin.” A protocol-relative URL also starts with a slash, and the browser treats it as a cross-host link on the current scheme.

What we shipped first

The renderer rewrote markdown links. https://… stayed a link. Paths that started with / stayed a link. Everything else became plain text. That looked like an allowlist.

//evil.example starts with /. The rewriter emitted <a href="//evil.example">. Anyone who could post markdown could ship a phishing host that inherited the page’s scheme.

The working shape

Allow https:// explicitly. Allow a same-origin path only when it starts with a single slash and does not start with two. Reject the rest as text.

// Reject protocol-relative before treating "/" as a path
function isSafeHref(url) {
  const t = String(url || "").trim();
  if (/^https:\/\//i.test(t)) return true;
  return t.startsWith("/") && !t.startsWith("//");
}

// Cases
isSafeHref("//evil.example");     // false — was wrongly true
isSafeHref("/workspace");         // true
isSafeHref("https://example.com"); // true
isSafeHref("javascript:alert(1)"); // false
isSafeHref("http://example.com");  // false

Copy-paste cases

Checklist

Try the lab

Same loop as a small repo: read the tree, run the failing test, fix the allowlist, re-run until it is green. New here? Type help. Stuck? help next.

Full page: Open this lab · All labs

Related: query parameters after the hash never arrive. How we deliver: methodology.

Engineering commentary only — not audit, legal, or certification advice.