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:
parent
30fcf1a58e
commit
4a0a993e81
6 changed files with 95 additions and 57 deletions
|
|
@ -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 = `
|
||||
<div style="font-weight: bold;">${callsign}</div>
|
||||
<div style="font-size: 10px; opacity: 0.8;">${altitude} • ${speed}</div>
|
||||
<div style="font-weight: bold;">${escapeHtml(callsign)}</div>
|
||||
<div style="font-size: 10px; opacity: 0.8;">${escapeHtml(altitude)} • ${escapeHtml(speed)}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 `
|
||||
<div class="aircraft-popup">
|
||||
<div class="popup-header">
|
||||
<div class="flight-info">
|
||||
<span class="icao-flag">${flag}</span>
|
||||
<span class="flight-id">${aircraft.ICAO24 || 'N/A'}</span>
|
||||
${aircraft.Callsign ? `→ <span class="callsign-loading" data-callsign="${aircraft.Callsign}"><span class="callsign">${aircraft.Callsign}</span></span>` : ''}
|
||||
<span class="icao-flag">${esc(flag)}</span>
|
||||
<span class="flight-id">${esc(aircraft.ICAO24) || 'N/A'}</span>
|
||||
${aircraft.Callsign ? `→ <span class="callsign-loading" data-callsign="${esc(aircraft.Callsign)}"><span class="callsign">${esc(aircraft.Callsign)}</span></span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="popup-details">
|
||||
<div class="detail-row">
|
||||
<strong>Country:</strong> ${country}
|
||||
<strong>Country:</strong> ${esc(country)}
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<strong>Type:</strong> ${type}
|
||||
<strong>Type:</strong> ${esc(type)}
|
||||
</div>
|
||||
${aircraft.TransponderCapability ? `
|
||||
<div class="detail-row">
|
||||
<strong>Transponder:</strong> ${aircraft.TransponderCapability}
|
||||
<strong>Transponder:</strong> ${esc(aircraft.TransponderCapability)}
|
||||
</div>` : ''}
|
||||
${aircraft.SignalQuality ? `
|
||||
<div class="detail-row">
|
||||
<strong>Signal Quality:</strong> ${aircraft.SignalQuality}
|
||||
<strong>Signal Quality:</strong> ${esc(aircraft.SignalQuality)}
|
||||
</div>` : ''}
|
||||
|
||||
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<div class="label">Altitude:</div>
|
||||
<div class="value${!altitude ? ' no-data' : ''}">${altitude ? `${altitude} ft | ${altitudeM} m` : 'N/A'}</div>
|
||||
<div class="value${!altitude ? ' no-data' : ''}">${altitude ? `${esc(altitude)} ft | ${esc(altitudeM)} m` : 'N/A'}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="label">Squawk:</div>
|
||||
<div class="value${!aircraft.Squawk ? ' no-data' : ''}">${aircraft.Squawk || 'N/A'}</div>
|
||||
<div class="value${!aircraft.Squawk ? ' no-data' : ''}">${esc(aircraft.Squawk) || 'N/A'}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="label">Speed:</div>
|
||||
<div class="value${!aircraft.GroundSpeed ? ' no-data' : ''}">${aircraft.GroundSpeed !== undefined && aircraft.GroundSpeed !== null ? `${aircraft.GroundSpeed} kt | ${speedKmh} km/h` : 'N/A'}</div>
|
||||
<div class="value${!aircraft.GroundSpeed ? ' no-data' : ''}">${aircraft.GroundSpeed !== undefined && aircraft.GroundSpeed !== null ? `${esc(aircraft.GroundSpeed)} kt | ${esc(speedKmh)} km/h` : 'N/A'}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="label">Track:</div>
|
||||
<div class="value${aircraft.Track === undefined || aircraft.Track === null ? ' no-data' : ''}">${aircraft.Track !== undefined && aircraft.Track !== null ? `${aircraft.Track}°` : 'N/A'}</div>
|
||||
<div class="value${aircraft.Track === undefined || aircraft.Track === null ? ' no-data' : ''}">${aircraft.Track !== undefined && aircraft.Track !== null ? `${esc(aircraft.Track)}°` : 'N/A'}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="label">V/Rate:</div>
|
||||
<div class="value${!aircraft.VerticalRate ? ' no-data' : ''}">${aircraft.VerticalRate ? `${aircraft.VerticalRate} ft/min` : 'N/A'}</div>
|
||||
<div class="value${!aircraft.VerticalRate ? ' no-data' : ''}">${aircraft.VerticalRate ? `${esc(aircraft.VerticalRate)} ft/min` : 'N/A'}</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="label">Distance:</div>
|
||||
<div class="value${distance ? '' : ' no-data'}">${distanceKm !== 'N/A' ? `${distanceKm} km` : 'N/A'}</div>
|
||||
<div class="value${distance ? '' : ' no-data'}">${distanceKm !== 'N/A' ? `${esc(distanceKm)} km` : 'N/A'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="detail-row">
|
||||
<strong>Position:</strong> ${aircraft.Latitude?.toFixed(4)}°, ${aircraft.Longitude?.toFixed(4)}°
|
||||
<strong>Position:</strong> ${esc(aircraft.Latitude?.toFixed(4))}°, ${esc(aircraft.Longitude?.toFixed(4))}°
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<strong>Messages:</strong> ${aircraft.TotalMessages || 0}
|
||||
<strong>Messages:</strong> ${esc(aircraft.TotalMessages || 0)}
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<strong>Age:</strong> ${this.calculateAge(aircraft).toFixed(1)}s
|
||||
<strong>Age:</strong> ${esc(this.calculateAge(aircraft).toFixed(1))}s
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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>`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
22
assets/static/js/modules/html-utils.js
Normal file
22
assets/static/js/modules/html-utils.js
Normal file
|
|
@ -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, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue