fix: Sanitize all innerHTML dynamic values to prevent XSS

Add centralized escapeHtml() utility and apply it to every dynamic value
inserted via innerHTML/template literals across the frontend. Data from
VRS JSON sources and external CSV files (airline names, countries) flows
through the backend as arbitrary strings that could contain HTML. While
Go's json.Marshal escapes < > &, JavaScript's JSON.parse reverses those
escapes before the values reach innerHTML — enabling script injection.

Affected modules: aircraft-manager, ui-manager, callsign-manager,
map-manager, and the 3D radar labels in app.js.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-02-13 15:21:56 +01:00
commit 4a0a993e81
6 changed files with 95 additions and 57 deletions

View file

@ -1,4 +1,6 @@
// Map and visualization management module
import { escapeHtml } from './html-utils.js';
export class MapManager {
constructor() {
this.map = null;
@ -177,18 +179,19 @@ export class MapManager {
}
createSourcePopupContent(source, aircraftData) {
const esc = escapeHtml;
const aircraftCount = aircraftData ? Array.from(aircraftData.values())
.filter(aircraft => aircraft.sources && aircraft.sources[source.id]).length : 0;
return `
<div class="source-popup">
<h3>${source.name}</h3>
<p><strong>ID:</strong> ${source.id}</p>
<p><strong>Location:</strong> ${source.latitude.toFixed(4)}°, ${source.longitude.toFixed(4)}°</p>
<h3>${esc(source.name)}</h3>
<p><strong>ID:</strong> ${esc(source.id)}</p>
<p><strong>Location:</strong> ${esc(source.latitude.toFixed(4))}°, ${esc(source.longitude.toFixed(4))}°</p>
<p><strong>Status:</strong> ${source.active ? 'Active' : 'Inactive'}</p>
<p><strong>Aircraft:</strong> ${aircraftCount}</p>
<p><strong>Messages:</strong> ${source.messages || 0}</p>
<p><strong>Last Seen:</strong> ${source.last_seen ? new Date(source.last_seen).toLocaleString() : 'N/A'}</p>
<p><strong>Aircraft:</strong> ${esc(aircraftCount)}</p>
<p><strong>Messages:</strong> ${esc(source.messages || 0)}</p>
<p><strong>Last Seen:</strong> ${source.last_seen ? esc(new Date(source.last_seen).toLocaleString()) : 'N/A'}</p>
</div>
`;
}
@ -205,11 +208,12 @@ export class MapManager {
);
for (const source of sortedSources) {
const esc = escapeHtml;
const item = document.createElement('div');
item.className = 'legend-item';
item.innerHTML = `
<span class="legend-icon" style="background: ${source.active ? '#00d4ff' : '#666666'}"></span>
<span title="${source.host}:${source.port}">${source.name}</span>
<span title="${esc(source.host)}:${esc(source.port)}">${esc(source.name)}</span>
`;
legend.appendChild(item);
}