diff --git a/assets/static/js/app.js b/assets/static/js/app.js index 836feb0..dd94eaa 100644 --- a/assets/static/js/app.js +++ b/assets/static/js/app.js @@ -8,6 +8,7 @@ import { AircraftManager } from './modules/aircraft-manager.js?v=2'; import { MapManager } from './modules/map-manager.js?v=2'; import { UIManager } from './modules/ui-manager.js?v=2'; import { CallsignManager } from './modules/callsign-manager.js'; +import { escapeHtml } from './modules/html-utils.js'; // Tile Management for 3D Map Base class TileManager { @@ -932,10 +933,10 @@ class SkyView { const callsign = aircraft.Callsign || aircraft.Icao || 'N/A'; const altitude = aircraft.Altitude ? `${Math.round(aircraft.Altitude)}ft` : 'N/A'; const speed = aircraft.GroundSpeed ? `${Math.round(aircraft.GroundSpeed)}kts` : 'N/A'; - + label.innerHTML = ` -
${callsign}
-
${altitude} • ${speed}
+
${escapeHtml(callsign)}
+
${escapeHtml(altitude)} • ${escapeHtml(speed)}
`; } diff --git a/assets/static/js/modules/aircraft-manager.js b/assets/static/js/modules/aircraft-manager.js index 3f018c6..06d3f5f 100644 --- a/assets/static/js/modules/aircraft-manager.js +++ b/assets/static/js/modules/aircraft-manager.js @@ -1,4 +1,6 @@ // Aircraft marker and data management module +import { escapeHtml } from './html-utils.js'; + export class AircraftManager { constructor(map, callsignManager = null) { this.map = map; @@ -437,67 +439,68 @@ export class AircraftManager { const distance = this.calculateDistance(aircraft); const distanceKm = distance ? (distance * 1.852).toFixed(1) : 'N/A'; + const esc = escapeHtml; return `
- +
diff --git a/assets/static/js/modules/callsign-manager.js b/assets/static/js/modules/callsign-manager.js index ef9a089..7556288 100644 --- a/assets/static/js/modules/callsign-manager.js +++ b/assets/static/js/modules/callsign-manager.js @@ -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 `${originalCallsign}`; + return `${esc(originalCallsign)}`; } return 'N/A'; } const parts = []; - + // Airline code if (callsignInfo.airline_code) { - parts.push(`${callsignInfo.airline_code}`); + parts.push(`${esc(callsignInfo.airline_code)}`); } // Flight number if (callsignInfo.flight_number) { - parts.push(`${callsignInfo.flight_number}`); + parts.push(`${esc(callsignInfo.flight_number)}`); } // Airline name (if available) let airlineInfo = ''; if (callsignInfo.airline_name) { - airlineInfo = ` - ${callsignInfo.airline_name} + airlineInfo = ` + ${esc(callsignInfo.airline_name)} `; - + // Add country if available if (callsignInfo.airline_country) { - airlineInfo += ` (${callsignInfo.airline_country})`; + airlineInfo += ` (${esc(callsignInfo.airline_country)})`; } } @@ -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 `${callsignInfo.display_name}`; + return `${esc(callsignInfo.display_name)}`; } - return `${callsignInfo.airline_code} ${callsignInfo.flight_number}`; + return `${esc(callsignInfo.airline_code)} ${esc(callsignInfo.flight_number)}`; } /** diff --git a/assets/static/js/modules/html-utils.js b/assets/static/js/modules/html-utils.js new file mode 100644 index 0000000..ac5bd11 --- /dev/null +++ b/assets/static/js/modules/html-utils.js @@ -0,0 +1,22 @@ +// HTML sanitization utilities to prevent XSS when using innerHTML +// +// Data from ADS-B/VRS sources and external CSV files (airline names, countries) +// flows through the Go backend as JSON. While json.Marshal escapes < > &, +// JSON.parse() reverses those escapes. Any dynamic value inserted via innerHTML +// or template literals must be escaped to prevent script injection. + +/** + * Escape a string for safe insertion into HTML content or attributes. + * Converts the five HTML-significant characters to their entity equivalents. + * @param {*} value - The value to escape (coerced to string, null/undefined become '') + * @returns {string} - HTML-safe string + */ +export function escapeHtml(value) { + if (value == null) return ''; + return String(value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} diff --git a/assets/static/js/modules/map-manager.js b/assets/static/js/modules/map-manager.js index 3149e51..dd754d1 100644 --- a/assets/static/js/modules/map-manager.js +++ b/assets/static/js/modules/map-manager.js @@ -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 `
-

${source.name}

-

ID: ${source.id}

-

Location: ${source.latitude.toFixed(4)}°, ${source.longitude.toFixed(4)}°

+

${esc(source.name)}

+

ID: ${esc(source.id)}

+

Location: ${esc(source.latitude.toFixed(4))}°, ${esc(source.longitude.toFixed(4))}°

Status: ${source.active ? 'Active' : 'Inactive'}

-

Aircraft: ${aircraftCount}

-

Messages: ${source.messages || 0}

-

Last Seen: ${source.last_seen ? new Date(source.last_seen).toLocaleString() : 'N/A'}

+

Aircraft: ${esc(aircraftCount)}

+

Messages: ${esc(source.messages || 0)}

+

Last Seen: ${source.last_seen ? esc(new Date(source.last_seen).toLocaleString()) : 'N/A'}

`; } @@ -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 = ` - ${source.name} + ${esc(source.name)} `; legend.appendChild(item); } diff --git a/assets/static/js/modules/ui-manager.js b/assets/static/js/modules/ui-manager.js index 9bb8618..374e8b7 100644 --- a/assets/static/js/modules/ui-manager.js +++ b/assets/static/js/modules/ui-manager.js @@ -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 = ` - ${icao} - ${aircraft.Callsign || '-'} + ${esc(icao)} + ${esc(aircraft.Callsign) || '-'} ${this.formatSquawk(aircraft)} - ${altitude ? `${altitude} ft` : '-'} - ${aircraft.GroundSpeed || '-'} kt - ${distance ? distance.toFixed(1) : '-'} km - ${aircraft.Track || '-'}° - ${sources} - ${bestSignal ? bestSignal.toFixed(1) : '-'} - ${this.calculateAge(aircraft).toFixed(0)}s + ${altitude ? `${esc(altitude)} ft` : '-'} + ${esc(aircraft.GroundSpeed) || '-'} kt + ${distance ? esc(distance.toFixed(1)) : '-'} km + ${esc(aircraft.Track) || '-'}° + ${esc(sources)} + ${bestSignal ? esc(bestSignal.toFixed(1)) : '-'} + ${esc(this.calculateAge(aircraft).toFixed(0))}s `; 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 `${aircraft.Squawk}`; + return `${esc(aircraft.Squawk)}`; } // Check if it's a special code (contains special emoji) else if (aircraft.SquawkDescription.includes('🔸')) { - return `${aircraft.Squawk}`; + return `${esc(aircraft.Squawk)}`; } // Check if it's a military code (contains military emoji) else if (aircraft.SquawkDescription.includes('🔰')) { - return `${aircraft.Squawk}`; + return `${esc(aircraft.Squawk)}`; } // Standard codes else { - return `${aircraft.Squawk}`; + return `${esc(aircraft.Squawk)}`; } } - + // No description available, show just the code - return aircraft.Squawk; + return esc(aircraft.Squawk); } updateSourceFilter() {