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 @@
// Callsign enrichment and display module
import { escapeHtml } from './html-utils.js';
export class CallsignManager {
constructor() {
this.callsignCache = new Map();
@ -82,36 +84,37 @@ export class CallsignManager {
* @returns {string} - HTML string for display
*/
generateCallsignDisplay(callsignInfo, originalCallsign = '') {
const esc = escapeHtml;
if (!callsignInfo || !callsignInfo.is_valid) {
// Fallback for invalid or missing callsign data
if (originalCallsign) {
return `<span class="callsign-display simple">${originalCallsign}</span>`;
return `<span class="callsign-display simple">${esc(originalCallsign)}</span>`;
}
return '<span class="callsign-display no-data">N/A</span>';
}
const parts = [];
// Airline code
if (callsignInfo.airline_code) {
parts.push(`<span class="airline-code">${callsignInfo.airline_code}</span>`);
parts.push(`<span class="airline-code">${esc(callsignInfo.airline_code)}</span>`);
}
// Flight number
if (callsignInfo.flight_number) {
parts.push(`<span class="flight-number">${callsignInfo.flight_number}</span>`);
parts.push(`<span class="flight-number">${esc(callsignInfo.flight_number)}</span>`);
}
// Airline name (if available)
let airlineInfo = '';
if (callsignInfo.airline_name) {
airlineInfo = `<span class="airline-name" title="${callsignInfo.airline_name}">
${callsignInfo.airline_name}
airlineInfo = `<span class="airline-name" title="${esc(callsignInfo.airline_name)}">
${esc(callsignInfo.airline_name)}
</span>`;
// Add country if available
if (callsignInfo.airline_country) {
airlineInfo += ` <span class="airline-country">(${callsignInfo.airline_country})</span>`;
airlineInfo += ` <span class="airline-country">(${esc(callsignInfo.airline_country)})</span>`;
}
}
@ -130,16 +133,17 @@ export class CallsignManager {
* @returns {string} - Compact HTML for table display
*/
generateCompactCallsignDisplay(callsignInfo, originalCallsign = '') {
const esc = escapeHtml;
if (!callsignInfo || !callsignInfo.is_valid) {
return originalCallsign || 'N/A';
return esc(originalCallsign) || 'N/A';
}
// For tables, use the display_name or format airline + flight
if (callsignInfo.display_name) {
return `<span class="callsign-compact" title="${callsignInfo.airline_name || ''}">${callsignInfo.display_name}</span>`;
return `<span class="callsign-compact" title="${esc(callsignInfo.airline_name || '')}">${esc(callsignInfo.display_name)}</span>`;
}
return `<span class="callsign-compact">${callsignInfo.airline_code} ${callsignInfo.flight_number}</span>`;
return `<span class="callsign-compact">${esc(callsignInfo.airline_code)} ${esc(callsignInfo.flight_number)}</span>`;
}
/**