Skip to content

Commit bd09aad

Browse files
Sanitize Wi-Fi scan rendering to prevent XSS from malicious SSIDs
1 parent 6a2e24a commit bd09aad

2 files changed

Lines changed: 89 additions & 13 deletions

File tree

nixos/admin-app/static/js/dashboard-app.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
document.addEventListener('click', function (event) {
5959
const networkButton = event.target.closest('[data-wifi-ssid]');
6060
if (!networkButton) return;
61-
D.selectWifiNetwork(decodeURIComponent(networkButton.dataset.wifiSsid), decodeURIComponent(networkButton.dataset.wifiFlags || ''));
61+
D.selectWifiNetwork(networkButton.dataset.wifiSsid || '', networkButton.dataset.wifiFlags || '');
6262
});
6363

6464
const wifiForm = document.getElementById('wifi-connect-form');

nixos/admin-app/static/js/dashboard-wifi.js

Lines changed: 88 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,89 @@
44
D.state.wifiSelectedFlags = '';
55
D.timers.wifiConnectPoll = null;
66

7+
D.clearWifiScanList = function () {
8+
const container = D.el('wifi-scan-list');
9+
if (container) {
10+
container.replaceChildren();
11+
}
12+
return container;
13+
};
14+
15+
D.renderWifiScanMessage = function (message, className) {
16+
const container = D.clearWifiScanList();
17+
if (!container) return;
18+
const text = document.createElement('p');
19+
text.className = className;
20+
text.textContent = message;
21+
container.appendChild(text);
22+
};
23+
24+
D.renderWifiScanLoading = function () {
25+
const container = D.clearWifiScanList();
26+
if (!container) return;
27+
28+
const wrapper = document.createElement('div');
29+
wrapper.className = 'flex items-center justify-center py-8';
30+
31+
const spinner = document.createElement('div');
32+
spinner.className = 'w-5 h-5 border-2 border-ln-pink border-t-transparent rounded-full animate-spin mr-3';
33+
34+
const label = document.createElement('span');
35+
label.className = 'text-ln-muted font-mono text-sm';
36+
label.textContent = 'Scanning...';
37+
38+
wrapper.appendChild(spinner);
39+
wrapper.appendChild(label);
40+
container.appendChild(wrapper);
41+
};
42+
43+
D.renderWifiScanResults = function (networks) {
44+
const container = D.clearWifiScanList();
45+
if (!container) return;
46+
47+
const list = document.createElement('div');
48+
list.className = 'space-y-1 max-h-72 overflow-y-auto';
49+
50+
networks.forEach(function (net) {
51+
const button = document.createElement('button');
52+
button.type = 'button';
53+
button.dataset.wifiSsid = net.ssid || '';
54+
button.dataset.wifiFlags = net.flags || '';
55+
button.className = 'wifi-network-btn w-full flex items-center justify-between px-3 py-2.5 rounded-lg hover:bg-ln-surface transition-colors group';
56+
57+
const left = document.createElement('div');
58+
left.className = 'flex items-center gap-2 min-w-0';
59+
60+
if (D.isEncrypted(net.flags)) {
61+
const lock = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
62+
lock.setAttribute('class', 'w-3.5 h-3.5 text-ln-muted shrink-0');
63+
lock.setAttribute('fill', 'currentColor');
64+
lock.setAttribute('viewBox', '0 0 20 20');
65+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
66+
path.setAttribute('fill-rule', 'evenodd');
67+
path.setAttribute('clip-rule', 'evenodd');
68+
path.setAttribute('d', 'M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z');
69+
lock.appendChild(path);
70+
left.appendChild(lock);
71+
}
72+
73+
const ssid = document.createElement('span');
74+
ssid.className = 'font-mono text-sm truncate';
75+
ssid.textContent = net.ssid || '';
76+
left.appendChild(ssid);
77+
78+
const signal = document.createElement('span');
79+
signal.className = 'font-mono text-sm text-ln-muted shrink-0 ml-2';
80+
signal.textContent = D.signalBars(net.signal);
81+
82+
button.appendChild(left);
83+
button.appendChild(signal);
84+
list.appendChild(button);
85+
});
86+
87+
container.appendChild(list);
88+
};
89+
790
D.signalBars = function (dbm) {
891
if (dbm >= -50) return '\u2582\u2584\u2586\u2588';
992
if (dbm >= -60) return '\u2582\u2584\u2586\u2007';
@@ -24,27 +107,20 @@
24107
D.openWifiScan = async function () {
25108
D.el('wifi-modal').classList.remove('hidden');
26109
D.showWifiView('wifi-scan-list');
27-
D.el('wifi-scan-list').innerHTML = '<div class="flex items-center justify-center py-8"><div class="w-5 h-5 border-2 border-ln-pink border-t-transparent rounded-full animate-spin mr-3"></div><span class="text-ln-muted font-mono text-sm">Scanning...</span></div>';
110+
D.renderWifiScanLoading();
28111
try {
29112
const resp = await fetch('/box/api/wifi/scan', { method: 'POST' });
30113
if (!resp.ok) throw new Error('Scan failed');
31114
const data = await resp.json();
32115
if (data.error) throw new Error(data.error);
33116
if (!data.networks || data.networks.length === 0) {
34-
D.el('wifi-scan-list').innerHTML = '<p class="text-ln-muted font-mono text-sm text-center py-8">No networks found</p>';
117+
D.renderWifiScanMessage('No networks found', 'text-ln-muted font-mono text-sm text-center py-8');
35118
return;
36119
}
37-
let html = '<div class="space-y-1 max-h-72 overflow-y-auto">';
38-
data.networks.forEach(function (net) {
39-
const lock = D.isEncrypted(net.flags) ? '<svg class="w-3.5 h-3.5 text-ln-muted shrink-0" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"></path></svg>' : '';
40-
html += '<button type="button" data-wifi-ssid="' + encodeURIComponent(net.ssid) + '" data-wifi-flags="' + encodeURIComponent(net.flags || '') + '" class="wifi-network-btn w-full flex items-center justify-between px-3 py-2.5 rounded-lg hover:bg-ln-surface transition-colors group">' +
41-
'<div class="flex items-center gap-2 min-w-0">' + lock + '<span class="font-mono text-sm truncate">' + net.ssid + '</span></div>' +
42-
'<span class="font-mono text-sm text-ln-muted shrink-0 ml-2">' + D.signalBars(net.signal) + '</span></button>';
43-
});
44-
html += '</div>';
45-
D.el('wifi-scan-list').innerHTML = html;
120+
D.renderWifiScanResults(data.networks);
46121
} catch (error) {
47-
D.el('wifi-scan-list').innerHTML = '<p class="text-red-400 font-mono text-sm text-center py-8">Scan failed: ' + error.message + '</p>';
122+
const message = error && error.message ? error.message : 'Scan failed';
123+
D.renderWifiScanMessage('Scan failed: ' + message, 'text-red-400 font-mono text-sm text-center py-8');
48124
}
49125
};
50126

0 commit comments

Comments
 (0)