donation item, shop soft open
All checks were successful
Deploy / build-deploy (push) Successful in 4m11s
All checks were successful
Deploy / build-deploy (push) Successful in 4m11s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b0666841f6
commit
abbd616b40
23 changed files with 2898 additions and 209 deletions
|
|
@ -69,8 +69,25 @@ int main() {
|
|||
// ── Url ───────────────────────────────────────────────────────────
|
||||
CheckEq(Url("href", "/shop/thing"), " href=\"/shop/thing\"", "url: site-relative");
|
||||
CheckEq(Url("href", "https://a.example/x"), " href=\"https://a.example/x\"", "url: https");
|
||||
// Plain http is on the allowlist too. Not merely tolerated: a link the
|
||||
// author wrote as http must survive as http rather than turn into an
|
||||
// inert "#", because a silently dead link is worse than an insecure one.
|
||||
CheckEq(Url("href", "http://x.example/a"), " href=\"http://x.example/a\"", "url: http");
|
||||
CheckEq(Url("href", "mailto:a@b.example"), " href=\"mailto:a@b.example\"", "url: mailto");
|
||||
CheckEq(Url("href", "#reviews"), " href=\"#reviews\"", "url: fragment");
|
||||
// The EIP-681 pay link the crypto rail hands the buyer. Eurc builds it as
|
||||
// ethereum:{contract}@{chainId}/transfer?address={to}&uint256={units}
|
||||
// and the order page emits it through Url(). Two things must hold at once:
|
||||
// ethereum: stays on the allowlist (drop it and every crypto pay button
|
||||
// becomes href="#", i.e. nobody on that rail can pay), and the query
|
||||
// separator is still escaped to & like any other attribute value.
|
||||
CheckEq(Url("href", "ethereum:0xAbC@8453/transfer?address=0xDeF&uint256=1000000"),
|
||||
" href=\"ethereum:0xAbC@8453/transfer?address=0xDeF&uint256=1000000\"",
|
||||
"url: ethereum: EIP-681 kept verbatim, ampersand escaped");
|
||||
// Empty href fails every allowlist branch — including the site-relative
|
||||
// one, which needs at least one character — so it lands on "#" rather
|
||||
// than emitting a link that resolves to the current page.
|
||||
CheckEq(Url("href", ""), " href=\"#\"", "url: empty falls back to #");
|
||||
// Escaping alone would NOT make these safe: they contain no character
|
||||
// that needs escaping, so only a scheme allowlist stops them.
|
||||
CheckEq(Url("href", "javascript:alert(1)"), " href=\"#\"", "url: javascript: neutralised");
|
||||
|
|
@ -99,6 +116,65 @@ int main() {
|
|||
CheckEq(Join(std::span<const Html::SafeHtml>{}), "", "join: empty");
|
||||
CheckEq(Escape("a") + Escape("<"), "a<", "operator+: escapes preserved");
|
||||
|
||||
// ── Autolink: escaping ────────────────────────────────────────────
|
||||
// Autolink, not Escape, is what every prose paragraph on /legal/*, /about
|
||||
// and /financials goes through (Views), and what Markdown hands its
|
||||
// paragraph bodies. So it is the real escaper on those pages: if it ever
|
||||
// stops escaping, that is stored XSS on the policy text.
|
||||
CheckEq(Autolink("<b>a & b</b>"), "<b>a & b</b>",
|
||||
"autolink: escapes text with no URL in it");
|
||||
// With a URL present the non-URL runs still go through Escape. The '<'
|
||||
// also doubles as a URL terminator here, which is why the anchor stops
|
||||
// before "</b>" instead of swallowing it into the href.
|
||||
CheckEq(Autolink("<b>https://x.example</b>"),
|
||||
"<b><a href=\"https://x.example\">https://x.example</a></b>",
|
||||
"autolink: markup around a URL stays escaped");
|
||||
// The URL text is escaped on BOTH sides of the anchor — attribute and
|
||||
// text node — because href goes through Url() (which calls Attr, which
|
||||
// escapes) and the label goes through Escape(). A '&' in a query string
|
||||
// is the everyday case that proves it.
|
||||
CheckEq(Autolink("https://x.example/?a=1&b=2"),
|
||||
"<a href=\"https://x.example/?a=1&b=2\">https://x.example/?a=1&b=2</a>",
|
||||
"autolink: ampersand escaped in href and in anchor text");
|
||||
// An explicit http/https scheme is required, so nothing else becomes a
|
||||
// link — least of all a scheme Url() would have had to neutralise.
|
||||
CheckEq(Autolink("ftp://x.example/a"), "ftp://x.example/a",
|
||||
"autolink: non-http scheme is not linked");
|
||||
CheckEq(Autolink("javascript:alert(1)"), "javascript:alert(1)",
|
||||
"autolink: javascript: is text, never an anchor");
|
||||
|
||||
// ── Autolink: URL boundaries ──────────────────────────────────────
|
||||
// The privacy notice ends a sentence with a bare address. A period pulled
|
||||
// into the href is a 404 for every reader who clicks it, so the trailing
|
||||
// sentence punctuation is trimmed back out of the URL and re-emitted as
|
||||
// escaped text after the </a>.
|
||||
CheckEq(Autolink("See https://catcrafts.net/analytics."),
|
||||
"See <a href=\"https://catcrafts.net/analytics\">"
|
||||
"https://catcrafts.net/analytics</a>.",
|
||||
"autolink: trailing period stays outside the anchor");
|
||||
// Same rule for a closing bracket the URL did not open: "(see .../y)" has
|
||||
// zero '(' inside the matched run and one ')', so the ')' is given back.
|
||||
CheckEq(Autolink("(see https://x.example/y)"),
|
||||
"(see <a href=\"https://x.example/y\">https://x.example/y</a>)",
|
||||
"autolink: unmatched closing paren stays outside the anchor");
|
||||
// But a bracket the URL DID open is part of it — one '(' and one ')' in
|
||||
// the run, so the count test holds and the paren is kept in both href and
|
||||
// text. Wikipedia disambiguation links are the reason this rule exists.
|
||||
CheckEq(Autolink("https://en.wikipedia.org/wiki/Foo_(bar) end"),
|
||||
"<a href=\"https://en.wikipedia.org/wiki/Foo_(bar)\">"
|
||||
"https://en.wikipedia.org/wiki/Foo_(bar)</a> end",
|
||||
"autolink: balanced paren kept inside the anchor");
|
||||
// A scheme has to start a word. The "https://" here begins at index 1
|
||||
// with a letter before it, so it is the tail of a longer token, not a
|
||||
// link — the guard that stops two run-together URLs linking the second.
|
||||
CheckEq(Autolink("shttps://x.example"), "shttps://x.example",
|
||||
"autolink: scheme mid-word is not a link");
|
||||
// A scheme with no host after it: find("//") lands at 6 and 6+2 is not
|
||||
// less than the 8-char run, so the degenerate case emits escaped text and
|
||||
// advances pos past it. That advance is the loop-stall guard.
|
||||
CheckEq(Autolink("https://"), "https://",
|
||||
"autolink: bare scheme emits text and cannot stall the loop");
|
||||
|
||||
if (failures != 0) {
|
||||
std::println(std::cerr, "{} check(s) failed", failures);
|
||||
return 1;
|
||||
|
|
|
|||
Loading…
Reference in a new issue