doxa-maps/library/utils/html.js
Doxa Maps 91c060974e Map popups show data as text: a people-group name or country field can no longer run script
The library's pin and region popups (useMapEvents) interpolated data fields raw into the HTML handed to
setHTML(); a name like <img src=x onerror=…> from an API or a partner's list would run in the host page
(CRITIC-3). library/utils/html.js adds escapeHtml and safeUrl (http(s) only); every field of both popups
goes through them. check:popup-escaping (headless, gates:post) mounts the real composable, renders both
popups from hostile data and requires no markup, handler or javascript: URL to survive: it FAILs on the
previous code (both popups) and PASSes here. check:cdn-pins and check:no-eval join gates:post too.
2026-09-26 05:50:24 +00:00

23 lines
1.2 KiB
JavaScript

/**
* html.js — the two helpers every HTML string built in library/ must use (CRITIC-3, 2026-09-26).
*
* Popups are built as HTML strings and handed to setHTML(), which parses them. A people-group name,
* a description or a country label comes from DATA (an API or a partner's list), so interpolating it
* raw lets a `<img onerror>` in the data run in the host page. Escape every data value; pass every URL
* through safeUrl so a `javascript:` link cannot ride in on a data field.
*/
const MAP = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;', '`': '&#96;' }
/** Text or attribute value → safe to interpolate into HTML (null/undefined → ''). */
export function escapeHtml(v) {
return v == null ? '' : String(v).replace(/[&<>"'`]/g, (c) => MAP[c])
}
/** An http(s) URL, escaped for an attribute; anything else (javascript:, data:, garbage) → fallback. */
export function safeUrl(u, fallback = '') {
try {
const url = new URL(String(u), typeof location !== 'undefined' ? location.href : 'https://example.invalid/')
if (url.protocol === 'http:' || url.protocol === 'https:') return escapeHtml(url.href)
} catch { /* not a URL */ }
return escapeHtml(fallback)
}