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 `
`;
}
@@ -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() {