pre-deploy: escape external feed strings in InfoBox HTML (ship-check)

Defense-in-depth for the public deploy: USGS/EONET place/title/category are now
HTML-escaped before interpolation into Cesium InfoBox descriptions, and the
USGS/EONET links are passed through lib.safeUrl (http(s)-only) so a hostile feed
can't inject markup or a javascript:/data: href. Entity names/labels stay raw
(Cesium renders those as text). Verified: escapeHtml neutralizes
<img onerror>, safeUrl drops javascript:/data:, 219 quakes + 500 fires still
render normally.

ship-check pass: no auth surface (dev-only history/snap not deployed); no
secrets in tree (AIS key empty, data/ gitignored); proxy upstreams are a fixed
dict (no SSRF); no money paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jing 2026-07-13 20:55:42 +10:00
parent b56684c6e6
commit 0162ec0cdf
3 changed files with 26 additions and 7 deletions

View File

@ -59,7 +59,7 @@ export default function create(ctx) {
if (!loc) continue;
const title = ev.title || 'Wildfire';
const category = (ev.categories && ev.categories[0] && ev.categories[0].title) || 'Wildfires';
const link = ev.link || (ev.id ? `https://eonet.gsfc.nasa.gov/api/v3/events/${ev.id}` : null);
const link = lib.safeUrl(ev.link || (ev.id ? `https://eonet.gsfc.nasa.gov/api/v3/events/${encodeURIComponent(ev.id)}` : ''));
const dateTxt = loc.date ? new Date(loc.date).toISOString().replace('T', ' ').replace('.000Z', ' UTC') : '—';
ds.entities.add({
@ -88,11 +88,11 @@ export default function create(ctx) {
},
description:
`<table class="cesium-infoBox-defaultTable"><tbody>` +
`<tr><th>Event</th><td>${title}</td></tr>` +
`<tr><th>Category</th><td>${category}</td></tr>` +
`<tr><th>Event</th><td>${lib.escapeHtml(title)}</td></tr>` +
`<tr><th>Category</th><td>${lib.escapeHtml(category)}</td></tr>` +
`<tr><th>Last report</th><td>${dateTxt}</td></tr>` +
`</tbody></table>` +
(link ? `<p><a href="${link}" target="_blank" rel="noopener">Open event on EONET ↗</a></p>` : '') +
(link ? `<p><a href="${lib.escapeHtml(link)}" target="_blank" rel="noopener">Open event on EONET ↗</a></p>` : '') +
`<p style="opacity:.7">Active wildfire event from NASA EONET (real data).</p>`,
});
count++;

View File

@ -64,7 +64,8 @@ export default function create(ctx) {
const depth = coords[2];
const mag = p.mag;
const timeMs = p.time;
const place = p.place || 'Unknown location';
const place = p.place || 'Unknown location'; // raw: used in name/label (text contexts)
const url = lib.safeUrl(p.url);
const size = baseSize(mag);
const color = magColor(mag);
const recentAtBuild = typeof timeMs === 'number' && (Date.now() - timeMs) < 3_600_000;
@ -93,11 +94,11 @@ export default function create(ctx) {
description:
`<table class="cesium-infoBox-defaultTable"><tbody>` +
`<tr><th>Magnitude</th><td>M${mag.toFixed(1)}</td></tr>` +
`<tr><th>Place</th><td>${place}</td></tr>` +
`<tr><th>Place</th><td>${lib.escapeHtml(place)}</td></tr>` +
`<tr><th>Depth</th><td>${depth != null ? depth.toFixed(1) + ' km' : '—'}</td></tr>` +
`<tr><th>Time (UTC)</th><td>${timeMs != null ? new Date(timeMs).toISOString().replace('T', ' ').replace('.000Z', ' UTC') : '—'}</td></tr>` +
`</tbody></table>` +
(p.url ? `<p><a href="${p.url}" target="_blank" rel="noopener">USGS event page ↗</a></p>` : ''),
(url ? `<p><a href="${lib.escapeHtml(url)}" target="_blank" rel="noopener">USGS event page ↗</a></p>` : ''),
});
// Labels only for the notable quakes, to keep the globe legible.

View File

@ -77,6 +77,24 @@ export function altitudeColor(CONFIG, altMeters) {
return c.aircraftHigh;
}
// Escape a string for safe interpolation into InfoBox description HTML.
// Feeds like USGS/EONET are trusted, but this is defense-in-depth for the
// public deploy (and the InfoBox is a real HTML sink).
export function escapeHtml(s) {
return String(s == null ? '' : s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
// Only allow http(s) URLs into href attributes (blocks javascript:/data: URLs).
export function safeUrl(u) {
const s = String(u == null ? '' : u).trim();
return /^https?:\/\//i.test(s) ? s : '';
}
// CSS hex → Cesium.Color, optional alpha.
export function cz(hex, alpha) {
const col = Cesium.Color.fromCssColorString(hex);