fix(dashboard): XSS escaping, ResizeObserver leak, WS reconnect timer

- Ajout de esc() dans api.js pour échapper les valeurs serveur avant injection innerHTML
- Application de esc() sur hostname, ip et agentId dans grid.js et popups.js
- Fix fuite mémoire ResizeObserver dans showDetail : déconnexion avant recréation (_resizeObs)
- Fix WebSocket reconnect : clearTimeout avant setTimeout pour éviter les timers concurrents (_reconnectTimer)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gilles Soulier
2026-05-22 12:46:37 +02:00
parent 555ddc3556
commit a19705ffda
4 changed files with 25 additions and 9 deletions
+11
View File
@@ -1,3 +1,14 @@
// Échappe les valeurs serveur avant injection dans innerHTML
function esc(s) {
if (s == null) return '—';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
const API = (() => {
const BASE = ''; // même origine, proxy Nginx vers le serveur Go
+3 -1
View File
@@ -1,6 +1,7 @@
const App = (() => {
let _ws = null;
let _reconnectDelay = 1000;
let _reconnectTimer = null;
let _serverConfig = null;
// Tooltip global position:fixed
@@ -62,7 +63,8 @@ const App = (() => {
};
_ws.onclose = () => {
setTimeout(connectWS, _reconnectDelay);
clearTimeout(_reconnectTimer);
_reconnectTimer = setTimeout(connectWS, _reconnectDelay);
_reconnectDelay = Math.min(_reconnectDelay * 2, 30000);
};
}
+3 -3
View File
@@ -61,12 +61,12 @@ const Grid = (() => {
<span style="display:flex;align-items:center;justify-content:center;width:100%;height:100%;color:var(--accent)">
<i class="fa-solid fa-server"></i></span>`;
return `<div class="tile ${sc}" id="tile-${id}" onclick="Popups.showDetail('${id}')">
return `<div class="tile ${sc}" id="tile-${id}" onclick="Popups.showDetail('${esc(id)}')">
<div class="tile-head">
<div class="t-icon">${iconContent}</div>
<div class="t-names">
<div class="t-host">${agent.hostname}</div>
<div class="t-ip">${agent.ip || '—'}</div>
<div class="t-host">${esc(agent.hostname)}</div>
<div class="t-ip">${esc(agent.ip) || '—'}</div>
</div>
<div class="t-led ${ledClass(agent.status)}"></div>
</div>
+8 -5
View File
@@ -1,6 +1,7 @@
const Popups = (() => {
let _currentAgentId = null;
let _agentCfgData = null;
let _resizeObs = null;
// ══ POPUP DÉTAIL ══
async function showDetail(agentId) {
@@ -50,7 +51,7 @@ const Popups = (() => {
const memPts = Charts.historyToMemPts(history);
const smartBtn = metrics?.smart
? `<div class="smart-btn ok" onclick="Popups.showSmart('${agentId}')" data-tip="Voir la santé complète du disque">
? `<div class="smart-btn ok" onclick="Popups.showSmart('${esc(agentId)}')" data-tip="Voir la santé complète du disque">
<div class="smart-dot"></div>
<span style="font-weight:600">SMART</span>
<span>·</span>
@@ -116,8 +117,8 @@ const Popups = (() => {
<div>
<div class="sec-title">INFORMATIONS</div>
<div class="meta-grid">
<div class="meta"><div class="meta-lbl">HOSTNAME</div><div class="meta-val">${agent.hostname}</div></div>
<div class="meta"><div class="meta-lbl">ADRESSE IP</div><div class="meta-val">${agent.ip || '—'}</div></div>
<div class="meta"><div class="meta-lbl">HOSTNAME</div><div class="meta-val">${esc(agent.hostname)}</div></div>
<div class="meta"><div class="meta-lbl">ADRESSE IP</div><div class="meta-val">${esc(agent.ip) || '—'}</div></div>
<div class="meta"><div class="meta-lbl">PROTOCOLES ACTIFS</div><div style="display:flex;gap:5px;margin-top:4px">${protos || '—'}</div></div>
<div class="meta"><div class="meta-lbl">DERNIER CONTACT</div><div class="meta-val">${new Date(agent.last_seen * 1000).toLocaleTimeString('fr-FR')}</div></div>
</div>
@@ -129,14 +130,16 @@ const Popups = (() => {
});
// Resize → sauvegarder sur serveur
if (_resizeObs) _resizeObs.disconnect();
const pd = document.getElementById('popup-detail');
new ResizeObserver(() => {
_resizeObs = new ResizeObserver(() => {
API.putServerConfig({
...App.serverConfig,
popup_detail_w: pd.offsetWidth,
popup_detail_h: pd.offsetHeight,
}).catch(() => {});
}).observe(pd);
});
_resizeObs.observe(pd);
document.getElementById('overlay-detail').style.display = 'flex';
}