a005acb93d
CI / full-gate (push) Successful in 2m38s
README loses the competitor comparison, the personas and the repeated philosophy; the
five near-identical E2E command blocks become a table plus one command, and the file
map a clause per entry. AGENTS.md keeps every decision but drops the narrative around
them. todo.md's completed items collapse to their task line — git holds the rest.
Comments lose restatement, README duplication and history ("used to", "originally",
dated notes). AGENTS.md gains a Prose discipline section making this a standing pass on
every change rather than a one-off cleanup.
src/compose.test.ts now expects 6 documented E2E run commands, not 10, since the README
states the command once instead of per suite.
29 lines
1.6 KiB
TypeScript
29 lines
1.6 KiB
TypeScript
// safeUrl(value) — a URL field is emitted verbatim into an href/src, so a `javascript:`/`data:`
|
|
// URL from untrusted data would be live XSS. Relative or http(s) passes,
|
|
// anything else collapses to "#". Exported to plugins via plugin-api.ts.
|
|
// localPath(value) — the redirect-URI allowlist for `return_to`: host-relative passes, absolute
|
|
// or protocol-relative is rejected, so a crafted value can't open-redirect.
|
|
|
|
// ASCII control chars + space that browsers strip/ignore when resolving a URL — strip them before
|
|
// the scheme check so "java\tscript:" / a leading space can't masquerade as relative.
|
|
const CONTROL_G = /[\u0000-\u0020\u007f]/g;
|
|
const CONTROL = /[\u0000-\u0020\u007f]/;
|
|
const HAS_SCHEME = /^[a-z][a-z0-9+.-]*:/i; // a URL scheme prefix, e.g. "javascript:", "http:"
|
|
const HTTP_SCHEME = /^https?:/i;
|
|
|
|
export function safeUrl(value: string): string {
|
|
const cleaned = value.replace(CONTROL_G, "");
|
|
if (!cleaned) return "#";
|
|
// A scheme present? Allow only http(s). No scheme ⇒ relative ⇒ safe. Return the original once
|
|
// deemed safe (EJS still HTML-escapes it into the attribute; the inert control chars don't matter).
|
|
if (HAS_SCHEME.test(cleaned) && !HTTP_SCHEME.test(cleaned)) return "#";
|
|
return value;
|
|
}
|
|
|
|
export function localPath(value: string | null | undefined): string | null {
|
|
if (!value || CONTROL.test(value)) return null;
|
|
if (!value.startsWith("/")) return null; // must be host-relative
|
|
if (value.startsWith("//") || value.startsWith("/\\")) return null; // protocol-relative ⇒ off-origin
|
|
return value;
|
|
}
|