Fix ADS-B sidebar deselect bug, ACARS XSS, and classifier dead code

- Clear sidebar highlights and ACARS message timer when stale selected
  aircraft is removed in cleanupOldAircraft()
- Escape all user-controlled strings in renderAcarsCard(),
  addAcarsMessage(), and renderAcarsMainCard() before innerHTML insertion
- Remove dead duplicate H1 check in classify_message_type
- Move _d label from link_test set to handshake return path

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
mitchross
2026-02-26 18:34:35 -05:00
parent 66f582f087
commit f652011366
4 changed files with 34 additions and 26 deletions
+23 -15
View File
@@ -3234,11 +3234,11 @@ sudo make install</code>
function renderAcarsCard(msg) { function renderAcarsCard(msg) {
const type = msg.message_type || 'other'; const type = msg.message_type || 'other';
const badge = getAcarsTypeBadge(type); const badge = getAcarsTypeBadge(type);
const desc = msg.label_description || ('Label ' + (msg.label || '?')); const desc = escapeHtml(msg.label_description || ('Label ' + (msg.label || '?')));
const text = msg.text || msg.msg || ''; const text = msg.text || msg.msg || '';
const truncText = text.length > 120 ? text.substring(0, 120) + '...' : text; const truncText = escapeHtml(text.length > 120 ? text.substring(0, 120) + '...' : text);
const time = msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString() : ''; const time = msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString() : '';
const flight = msg.flight || ''; const flight = escapeHtml(msg.flight || '');
let parsedHtml = ''; let parsedHtml = '';
if (msg.parsed) { if (msg.parsed) {
@@ -3246,23 +3246,23 @@ sudo make install</code>
if (type === 'position' && p.lat !== undefined) { if (type === 'position' && p.lat !== undefined) {
parsedHtml = '<div style="color:var(--accent-green);margin-top:2px;">' + parsedHtml = '<div style="color:var(--accent-green);margin-top:2px;">' +
p.lat.toFixed(4) + ', ' + p.lon.toFixed(4) + p.lat.toFixed(4) + ', ' + p.lon.toFixed(4) +
(p.flight_level ? ' • ' + p.flight_level : '') + (p.flight_level ? ' • ' + escapeHtml(String(p.flight_level)) : '') +
(p.destination ? ' → ' + p.destination : '') + '</div>'; (p.destination ? ' → ' + escapeHtml(String(p.destination)) : '') + '</div>';
} else if (type === 'engine_data') { } else if (type === 'engine_data') {
const parts = []; const parts = [];
Object.keys(p).forEach(k => { Object.keys(p).forEach(k => {
parts.push(k + ': ' + p[k].value); parts.push(escapeHtml(k) + ': ' + escapeHtml(String(p[k].value)));
}); });
if (parts.length) { if (parts.length) {
parsedHtml = '<div style="color:var(--accent-orange,#ff9500);margin-top:2px;">' + parts.slice(0, 4).join(' | ') + '</div>'; parsedHtml = '<div style="color:var(--accent-orange,#ff9500);margin-top:2px;">' + parts.slice(0, 4).join(' | ') + '</div>';
} }
} else if (type === 'oooi' && p.origin) { } else if (type === 'oooi' && p.origin) {
parsedHtml = '<div style="color:var(--accent-cyan);margin-top:2px;">' + parsedHtml = '<div style="color:var(--accent-cyan);margin-top:2px;">' +
p.origin + ' → ' + p.destination + escapeHtml(String(p.origin)) + ' → ' + escapeHtml(String(p.destination)) +
(p.out ? ' | OUT ' + p.out : '') + (p.out ? ' | OUT ' + escapeHtml(String(p.out)) : '') +
(p.off ? ' OFF ' + p.off : '') + (p.off ? ' OFF ' + escapeHtml(String(p.off)) : '') +
(p.on ? ' ON ' + p.on : '') + (p.on ? ' ON ' + escapeHtml(String(p.on)) : '') +
(p['in'] ? ' IN ' + p['in'] : '') + '</div>'; (p['in'] ? ' IN ' + escapeHtml(String(p['in'])) : '') + '</div>';
} }
} }
@@ -3341,6 +3341,8 @@ sudo make install</code>
selectedIcao = null; selectedIcao = null;
showAircraftDetails(null); showAircraftDetails(null);
updateFlightLookupBtn(); updateFlightLookupBtn();
highlightSidebarMessages(null);
clearAircraftMessages();
} }
} }
}); });
@@ -4096,6 +4098,12 @@ sudo make install</code>
const text = data.text || data.msg || ''; const text = data.text || data.msg || '';
const time = new Date().toLocaleTimeString(); const time = new Date().toLocaleTimeString();
// Escape user-controlled strings for safe innerHTML insertion
const eFlight = escapeHtml(flight);
const eReg = escapeHtml(reg);
const eLabelDesc = escapeHtml(labelDesc || (label ? 'Label: ' + label : ''));
const eText = escapeHtml(text.length > 80 ? text.substring(0, 80) + '...' : text);
// Try to find matching tracked aircraft // Try to find matching tracked aircraft
const matchedIcao = findAircraftIcaoByFlight(flight) || const matchedIcao = findAircraftIcaoByFlight(flight) ||
findAircraftIcaoByFlight(data.tail) || findAircraftIcaoByFlight(data.tail) ||
@@ -4120,12 +4128,12 @@ sudo make install</code>
msg.innerHTML = ` msg.innerHTML = `
<div style="display: flex; justify-content: space-between; margin-bottom: 2px;"> <div style="display: flex; justify-content: space-between; margin-bottom: 2px;">
<span style="color: var(--accent-cyan); font-weight: bold;">${flight}${linkIcon}</span> <span style="color: var(--accent-cyan); font-weight: bold;">${eFlight}${linkIcon}</span>
<span style="color: var(--text-muted);">${time}</span> <span style="color: var(--text-muted);">${time}</span>
</div> </div>
${reg ? `<div style="color: var(--text-muted); font-size: 9px;">Reg: ${reg}</div>` : ''} ${reg ? `<div style="color: var(--text-muted); font-size: 9px;">Reg: ${eReg}</div>` : ''}
<div style="margin-top: 2px;">${typeBadge} <span style="color: var(--text-primary);">${labelDesc || (label ? 'Label: ' + label : '')}</span></div> <div style="margin-top: 2px;">${typeBadge} <span style="color: var(--text-primary);">${eLabelDesc}</span></div>
${text && msgType !== 'link_test' && msgType !== 'handshake' ? `<div style="color: var(--text-dim); margin-top: 3px; word-break: break-word; font-size: 9px;">${text.length > 80 ? text.substring(0, 80) + '...' : text}</div>` : ''} ${text && msgType !== 'link_test' && msgType !== 'handshake' ? `<div style="color: var(--text-dim); margin-top: 3px; word-break: break-word; font-size: 9px;">${eText}</div>` : ''}
`; `;
container.insertBefore(msg, container.firstChild); container.insertBefore(msg, container.firstChild);
+6 -6
View File
@@ -189,25 +189,25 @@
} }
function renderAcarsMainCard(data) { function renderAcarsMainCard(data) {
const flight = data.flight || 'UNKNOWN'; const flight = escapeHtml(data.flight || 'UNKNOWN');
const type = data.message_type || 'other'; const type = data.message_type || 'other';
const badge = acarsMainTypeBadge(type); const badge = acarsMainTypeBadge(type);
const desc = data.label_description || (data.label ? 'Label: ' + data.label : ''); const desc = escapeHtml(data.label_description || (data.label ? 'Label: ' + data.label : ''));
const text = data.text || data.msg || ''; const text = data.text || data.msg || '';
const truncText = text.length > 150 ? text.substring(0, 150) + '...' : text; const truncText = escapeHtml(text.length > 150 ? text.substring(0, 150) + '...' : text);
const time = new Date().toLocaleTimeString(); const time = new Date().toLocaleTimeString();
let parsedHtml = ''; let parsedHtml = '';
if (data.parsed) { if (data.parsed) {
const p = data.parsed; const p = data.parsed;
if (type === 'position' && p.lat !== undefined) { if (type === 'position' && p.lat !== undefined) {
parsedHtml = `<div style="color:var(--accent-green);margin-top:2px;font-size:10px;">${p.lat.toFixed(4)}, ${p.lon.toFixed(4)}${p.flight_level ? ' &bull; ' + p.flight_level : ''}${p.destination ? ' &rarr; ' + p.destination : ''}</div>`; parsedHtml = `<div style="color:var(--accent-green);margin-top:2px;font-size:10px;">${p.lat.toFixed(4)}, ${p.lon.toFixed(4)}${p.flight_level ? ' &bull; ' + escapeHtml(String(p.flight_level)) : ''}${p.destination ? ' &rarr; ' + escapeHtml(String(p.destination)) : ''}</div>`;
} else if (type === 'engine_data') { } else if (type === 'engine_data') {
const parts = []; const parts = [];
Object.keys(p).forEach(k => parts.push(k + ': ' + p[k].value)); Object.keys(p).forEach(k => parts.push(escapeHtml(k) + ': ' + escapeHtml(String(p[k].value))));
if (parts.length) parsedHtml = `<div style="color:#ff9500;margin-top:2px;font-size:10px;">${parts.slice(0, 4).join(' | ')}</div>`; if (parts.length) parsedHtml = `<div style="color:#ff9500;margin-top:2px;font-size:10px;">${parts.slice(0, 4).join(' | ')}</div>`;
} else if (type === 'oooi' && p.origin) { } else if (type === 'oooi' && p.origin) {
parsedHtml = `<div style="color:var(--accent-cyan);margin-top:2px;font-size:10px;">${p.origin} &rarr; ${p.destination}${p.out ? ' | OUT ' + p.out : ''}${p.off ? ' OFF ' + p.off : ''}${p.on ? ' ON ' + p.on : ''}${p['in'] ? ' IN ' + p['in'] : ''}</div>`; parsedHtml = `<div style="color:var(--accent-cyan);margin-top:2px;font-size:10px;">${escapeHtml(String(p.origin))} &rarr; ${escapeHtml(String(p.destination))}${p.out ? ' | OUT ' + escapeHtml(String(p.out)) : ''}${p.off ? ' OFF ' + escapeHtml(String(p.off)) : ''}${p.on ? ' ON ' + escapeHtml(String(p.on)) : ''}${p['in'] ? ' IN ' + escapeHtml(String(p['in'])) : ''}</div>`;
} }
} }
+3 -3
View File
@@ -63,8 +63,8 @@ class TestClassifyMessageType:
def test_sq_is_squawk(self): def test_sq_is_squawk(self):
assert classify_message_type('SQ') == 'squawk' assert classify_message_type('SQ') == 'squawk'
def test_underscore_d_is_link_test(self): def test_underscore_d_is_handshake(self):
assert classify_message_type('_d') == 'link_test' assert classify_message_type('_d') == 'handshake'
def test_q0_is_link_test(self): def test_q0_is_link_test(self):
assert classify_message_type('Q0') == 'link_test' assert classify_message_type('Q0') == 'link_test'
@@ -228,7 +228,7 @@ class TestTranslateMessage:
msg = {'label': '_d', 'text': ''} msg = {'label': '_d', 'text': ''}
result = translate_message(msg) result = translate_message(msg)
assert result['label_description'] == 'Demand mode (link test)' assert result['label_description'] == 'Demand mode (link test)'
assert result['message_type'] in ('link_test', 'handshake') assert result['message_type'] == 'handshake'
def test_unknown_label(self): def test_unknown_label(self):
msg = {'label': 'ZZ', 'text': 'SOME DATA'} msg = {'label': 'ZZ', 'text': 'SOME DATA'}
+2 -2
View File
@@ -127,7 +127,7 @@ def classify_message_type(label: str | None, text: str | None = None) -> str:
# Position reports # Position reports
if label in ('H1', '20', '15', '16', '30', 'S1'): if label in ('H1', '20', '15', '16', '30', 'S1'):
return 'position' return 'position'
if label == 'H1' or (text and '#M1BPOS' in text): if text and '#M1BPOS' in text:
return 'position' return 'position'
# Engine / DFDR data # Engine / DFDR data
@@ -155,7 +155,7 @@ def classify_message_type(label: str | None, text: str | None = None) -> str:
return 'squawk' return 'squawk'
# Link test / handshake # Link test / handshake
if label in ('_d', 'Q0', 'QA', 'QB', 'QC', 'QD', 'QE', 'QF', 'QG', if label in ('Q0', 'QA', 'QB', 'QC', 'QD', 'QE', 'QF', 'QG',
'QH', 'QK', 'QM', 'QN', 'QP', 'QQ', 'QR', 'QS', 'QT', 'QX', 'QH', 'QK', 'QM', 'QN', 'QP', 'QQ', 'QR', 'QS', 'QT', 'QX',
'4X'): '4X'):
return 'link_test' return 'link_test'