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 @@
// UI and table management module
import { escapeHtml } from './html-utils.js';
export class UIManager {
constructor() {
this.aircraftData = new Map();
@ -118,18 +120,19 @@ export class UIManager {
const sources = aircraft.sources ? Object.keys(aircraft.sources).length : 0;
const bestSignal = this.getBestSignalFromSources(aircraft.sources);
const esc = escapeHtml;
const row = document.createElement('tr');
row.innerHTML = `
<td><span class="type-badge ${type}">${icao}</span></td>
<td>${aircraft.Callsign || '-'}</td>
<td><span class="type-badge ${esc(type)}">${esc(icao)}</span></td>
<td>${esc(aircraft.Callsign) || '-'}</td>
<td>${this.formatSquawk(aircraft)}</td>
<td>${altitude ? `${altitude} ft` : '-'}</td>
<td>${aircraft.GroundSpeed || '-'} kt</td>
<td>${distance ? distance.toFixed(1) : '-'} km</td>
<td>${aircraft.Track || '-'}°</td>
<td>${sources}</td>
<td><span class="${this.getSignalClass(bestSignal)}">${bestSignal ? bestSignal.toFixed(1) : '-'}</span></td>
<td>${this.calculateAge(aircraft).toFixed(0)}s</td>
<td>${altitude ? `${esc(altitude)} ft` : '-'}</td>
<td>${esc(aircraft.GroundSpeed) || '-'} kt</td>
<td>${distance ? esc(distance.toFixed(1)) : '-'} km</td>
<td>${esc(aircraft.Track) || '-'}°</td>
<td>${esc(sources)}</td>
<td><span class="${esc(this.getSignalClass(bestSignal))}">${bestSignal ? esc(bestSignal.toFixed(1)) : '-'}</span></td>
<td>${esc(this.calculateAge(aircraft).toFixed(0))}s</td>
`;
row.addEventListener('click', () => {
@ -192,29 +195,30 @@ export class UIManager {
formatSquawk(aircraft) {
if (!aircraft.Squawk) return '-';
const esc = escapeHtml;
// If we have a description, format it nicely
if (aircraft.SquawkDescription) {
// Check if it's an emergency code (contains warning emoji)
if (aircraft.SquawkDescription.includes('⚠️')) {
return `<span class="squawk-emergency" title="${aircraft.SquawkDescription}">${aircraft.Squawk}</span>`;
return `<span class="squawk-emergency" title="${esc(aircraft.SquawkDescription)}">${esc(aircraft.Squawk)}</span>`;
}
// Check if it's a special code (contains special emoji)
else if (aircraft.SquawkDescription.includes('🔸')) {
return `<span class="squawk-special" title="${aircraft.SquawkDescription}">${aircraft.Squawk}</span>`;
return `<span class="squawk-special" title="${esc(aircraft.SquawkDescription)}">${esc(aircraft.Squawk)}</span>`;
}
// Check if it's a military code (contains military emoji)
else if (aircraft.SquawkDescription.includes('🔰')) {
return `<span class="squawk-military" title="${aircraft.SquawkDescription}">${aircraft.Squawk}</span>`;
return `<span class="squawk-military" title="${esc(aircraft.SquawkDescription)}">${esc(aircraft.Squawk)}</span>`;
}
// Standard codes
else {
return `<span class="squawk-standard" title="${aircraft.SquawkDescription}">${aircraft.Squawk}</span>`;
return `<span class="squawk-standard" title="${esc(aircraft.SquawkDescription)}">${esc(aircraft.Squawk)}</span>`;
}
}
// No description available, show just the code
return aircraft.Squawk;
return esc(aircraft.Squawk);
}
updateSourceFilter() {