diff --git a/DECISIONS.md b/DECISIONS.md index 5bda840..7281cd4 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -32,6 +32,18 @@ This file documents major architectural decisions and their rationale. --- +## Decision: XSS Prevention in Mission Control Dashboard + +**Date:** 2026-03-02 + +**Context:** The Mission Control dashboard was using `innerHTML` to render dependency details and recommendations from the `/health/sovereignty` endpoint. While these sources are currently internal, using `innerHTML` with dynamic data is a security risk and violates the "Non-Negotiable Rules" in `AGENTS.md`. + +**Decision:** Refactored the JavaScript in `mission_control.html` to use `document.createElement` and `textContent` for all dynamic data rendering. + +**Rationale:** This approach provides built-in XSS protection by ensuring that any data from the API is treated as plain text rather than HTML, fulfilling the security requirements of the project. + +--- + ## Add New Decisions Above This Line When making significant architectural choices, document: diff --git a/src/dashboard/templates/mission_control.html b/src/dashboard/templates/mission_control.html index c1bad5e..8e5b64e 100644 --- a/src/dashboard/templates/mission_control.html +++ b/src/dashboard/templates/mission_control.html @@ -203,27 +203,51 @@ async function loadSovereignty() { const scoreColor = dep.sovereignty_score >= 9 ? 'var(--success)' : dep.sovereignty_score >= 7 ? 'var(--warning)' : 'var(--danger)'; - card.innerHTML = ` -
- ${dep.name} - ${dep.status} -
-
- ${dep.details.error || dep.details.note || 'Operating normally'} -
-
- Sovereignty: ${dep.sovereignty_score}/10 -
- `; + // Securely build card content using textContent for dynamic data + const header = document.createElement('div'); + header.style = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;'; + + const name = document.createElement('strong'); + name.textContent = dep.name; + + const badge = document.createElement('span'); + badge.className = 'badge'; + badge.style.background = statusColor; + badge.textContent = dep.status; + + header.appendChild(name); + header.appendChild(badge); + + const details = document.createElement('div'); + details.style = 'font-size: 0.875rem; color: var(--text-muted); margin-bottom: 8px;'; + details.textContent = dep.details.error || dep.details.note || 'Operating normally'; + + const score = document.createElement('div'); + score.style = `font-size: 0.75rem; color: ${scoreColor};`; + score.textContent = `Sovereignty: ${dep.sovereignty_score}/10`; + + card.appendChild(header); + card.appendChild(details); + card.appendChild(score); grid.appendChild(card); }); - // Update recommendations + // Update recommendations securely const recs = document.getElementById('recommendations'); + recs.innerHTML = ''; if (data.recommendations && data.recommendations.length > 0) { - recs.innerHTML = ''; + const ul = document.createElement('ul'); + data.recommendations.forEach(r => { + const li = document.createElement('li'); + li.textContent = r; + ul.appendChild(li); + }); + recs.appendChild(ul); } else { - recs.innerHTML = '

No recommendations — system optimal

'; + const p = document.createElement('p'); + p.style.color = 'var(--text-muted)'; + p.textContent = 'No recommendations — system optimal'; + recs.appendChild(p); } } catch (error) {