Skip to content

Commit 4878f9b

Browse files
committed
fix: escape YAML data in threat model detail overlay to prevent XSS
The showThreatDetail() function in all four threat model pages (en, ja, ko, zh-cn) concatenates YAML-sourced threat data directly into innerHTML without sanitization. Because the trust page invites community contributions to the threat model via pull requests, a malicious PR modifying threats.yaml could inject arbitrary HTML/JS that executes when a visitor clicks any threat card. Add an escapeHtml() helper that encodes &, <, >, ", and ' and wrap all data fields (risk, atlas, description, attackVector, affected, mitigations, residualRisk, recommendations) and i18n label values before innerHTML insertion. Existing textContent assignments (detailId, detailTitle) are already safe and left unchanged. Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca>
1 parent f69ef87 commit 4878f9b

4 files changed

Lines changed: 20 additions & 8 deletions

File tree

src/pages/trust/ja/threatmodel.astro

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -746,14 +746,17 @@ for (const b of trustBoundaries) {
746746
function getRiskColor(risk) {
747747
switch (risk) { case 'Critical': return '#ef4444'; case 'High': return '#f97316'; case 'Medium': return '#eab308'; case 'Low': return '#22c55e'; default: return '#6b7280'; }
748748
}
749+
function escapeHtml(s) {
750+
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
751+
}
749752
function showThreatDetail(threatId) {
750753
var threat = threats.find(function(t) { return t.id === threatId; });
751754
if (!threat) return;
752755
document.getElementById('detailId').textContent = threat.id + ' \u2022 ' + threat.tactic;
753756
document.getElementById('detailTitle').textContent = threat.name;
754757
var rc = getRiskColor(threat.risk);
755-
document.getElementById('detailBadges').innerHTML = '<span class="detail-badge" style="background:' + rc + '12;color:' + rc + '">' + threat.risk + ' ' + detailLabels.riskSuffix + '</span><a href="https://atlas.mitre.org/techniques/' + threat.atlas + '" target="_blank" class="detail-badge atlas-link" style="background:rgba(255,90,54,0.08)">' + threat.atlas + '</a>';
756-
document.getElementById('detailBody').innerHTML = '<div class="detail-section"><div class="detail-label">' + detailLabels.description + '</div><div class="detail-value">' + threat.description + '</div></div><div class="detail-section"><div class="detail-label">' + detailLabels.attackVector + '</div><div class="detail-value">' + threat.attackVector + '</div></div><div class="detail-section"><div class="detail-label">' + detailLabels.affected + '</div><div class="detail-value">' + threat.affected + '</div></div><div class="detail-section"><div class="detail-label">' + detailLabels.mitigations + '</div><div class="detail-value">' + threat.mitigations + '</div></div><div class="detail-section"><div class="detail-label">' + detailLabels.residualRisk + '</div><div class="detail-value">' + threat.residualRisk + '</div></div><div class="detail-section"><div class="detail-label">' + detailLabels.recommendations + '</div><div class="detail-value">' + threat.recommendations + '</div></div>';
758+
document.getElementById('detailBadges').innerHTML = '<span class="detail-badge" style="background:' + rc + '12;color:' + rc + '">' + escapeHtml(threat.risk) + ' ' + escapeHtml(detailLabels.riskSuffix) + '</span><a href="https://atlas.mitre.org/techniques/' + escapeHtml(threat.atlas) + '" target="_blank" class="detail-badge atlas-link" style="background:rgba(255,90,54,0.08)">' + escapeHtml(threat.atlas) + '</a>';
759+
document.getElementById('detailBody').innerHTML = '<div class="detail-section"><div class="detail-label">' + escapeHtml(detailLabels.description) + '</div><div class="detail-value">' + escapeHtml(threat.description) + '</div></div><div class="detail-section"><div class="detail-label">' + escapeHtml(detailLabels.attackVector) + '</div><div class="detail-value">' + escapeHtml(threat.attackVector) + '</div></div><div class="detail-section"><div class="detail-label">' + escapeHtml(detailLabels.affected) + '</div><div class="detail-value">' + escapeHtml(threat.affected) + '</div></div><div class="detail-section"><div class="detail-label">' + escapeHtml(detailLabels.mitigations) + '</div><div class="detail-value">' + escapeHtml(threat.mitigations) + '</div></div><div class="detail-section"><div class="detail-label">' + escapeHtml(detailLabels.residualRisk) + '</div><div class="detail-value">' + escapeHtml(threat.residualRisk) + '</div></div><div class="detail-section"><div class="detail-label">' + escapeHtml(detailLabels.recommendations) + '</div><div class="detail-value">' + escapeHtml(threat.recommendations) + '</div></div>';
757760
document.getElementById('detailOverlay').classList.add('active');
758761
document.querySelectorAll('.threat-card').forEach(function(c) { c.classList.remove('selected'); });
759762
var sel = document.querySelector('[data-threat-id="' + threatId + '"]');

src/pages/trust/ko/threatmodel.astro

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -746,14 +746,17 @@ for (const b of trustBoundaries) {
746746
function getRiskColor(risk) {
747747
switch (risk) { case 'Critical': return '#ef4444'; case 'High': return '#f97316'; case 'Medium': return '#eab308'; case 'Low': return '#22c55e'; default: return '#6b7280'; }
748748
}
749+
function escapeHtml(s) {
750+
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
751+
}
749752
function showThreatDetail(threatId) {
750753
var threat = threats.find(function(t) { return t.id === threatId; });
751754
if (!threat) return;
752755
document.getElementById('detailId').textContent = threat.id + ' \u2022 ' + threat.tactic;
753756
document.getElementById('detailTitle').textContent = threat.name;
754757
var rc = getRiskColor(threat.risk);
755-
document.getElementById('detailBadges').innerHTML = '<span class="detail-badge" style="background:' + rc + '12;color:' + rc + '">' + threat.risk + ' ' + detailLabels.riskSuffix + '</span><a href="https://atlas.mitre.org/techniques/' + threat.atlas + '" target="_blank" class="detail-badge atlas-link" style="background:rgba(255,90,54,0.08)">' + threat.atlas + '</a>';
756-
document.getElementById('detailBody').innerHTML = '<div class="detail-section"><div class="detail-label">' + detailLabels.description + '</div><div class="detail-value">' + threat.description + '</div></div><div class="detail-section"><div class="detail-label">' + detailLabels.attackVector + '</div><div class="detail-value">' + threat.attackVector + '</div></div><div class="detail-section"><div class="detail-label">' + detailLabels.affected + '</div><div class="detail-value">' + threat.affected + '</div></div><div class="detail-section"><div class="detail-label">' + detailLabels.mitigations + '</div><div class="detail-value">' + threat.mitigations + '</div></div><div class="detail-section"><div class="detail-label">' + detailLabels.residualRisk + '</div><div class="detail-value">' + threat.residualRisk + '</div></div><div class="detail-section"><div class="detail-label">' + detailLabels.recommendations + '</div><div class="detail-value">' + threat.recommendations + '</div></div>';
758+
document.getElementById('detailBadges').innerHTML = '<span class="detail-badge" style="background:' + rc + '12;color:' + rc + '">' + escapeHtml(threat.risk) + ' ' + escapeHtml(detailLabels.riskSuffix) + '</span><a href="https://atlas.mitre.org/techniques/' + escapeHtml(threat.atlas) + '" target="_blank" class="detail-badge atlas-link" style="background:rgba(255,90,54,0.08)">' + escapeHtml(threat.atlas) + '</a>';
759+
document.getElementById('detailBody').innerHTML = '<div class="detail-section"><div class="detail-label">' + escapeHtml(detailLabels.description) + '</div><div class="detail-value">' + escapeHtml(threat.description) + '</div></div><div class="detail-section"><div class="detail-label">' + escapeHtml(detailLabels.attackVector) + '</div><div class="detail-value">' + escapeHtml(threat.attackVector) + '</div></div><div class="detail-section"><div class="detail-label">' + escapeHtml(detailLabels.affected) + '</div><div class="detail-value">' + escapeHtml(threat.affected) + '</div></div><div class="detail-section"><div class="detail-label">' + escapeHtml(detailLabels.mitigations) + '</div><div class="detail-value">' + escapeHtml(threat.mitigations) + '</div></div><div class="detail-section"><div class="detail-label">' + escapeHtml(detailLabels.residualRisk) + '</div><div class="detail-value">' + escapeHtml(threat.residualRisk) + '</div></div><div class="detail-section"><div class="detail-label">' + escapeHtml(detailLabels.recommendations) + '</div><div class="detail-value">' + escapeHtml(threat.recommendations) + '</div></div>';
757760
document.getElementById('detailOverlay').classList.add('active');
758761
document.querySelectorAll('.threat-card').forEach(function(c) { c.classList.remove('selected'); });
759762
var sel = document.querySelector('[data-threat-id="' + threatId + '"]');

src/pages/trust/threatmodel.astro

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -745,14 +745,17 @@ for (const b of trustBoundaries) {
745745
function getRiskColor(risk) {
746746
switch (risk) { case 'Critical': return '#ef4444'; case 'High': return '#f97316'; case 'Medium': return '#eab308'; case 'Low': return '#22c55e'; default: return '#6b7280'; }
747747
}
748+
function escapeHtml(s) {
749+
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
750+
}
748751
function showThreatDetail(threatId) {
749752
var threat = threats.find(function(t) { return t.id === threatId; });
750753
if (!threat) return;
751754
document.getElementById('detailId').textContent = threat.id + ' \u2022 ' + threat.tactic;
752755
document.getElementById('detailTitle').textContent = threat.name;
753756
var rc = getRiskColor(threat.risk);
754-
document.getElementById('detailBadges').innerHTML = '<span class="detail-badge" style="background:' + rc + '12;color:' + rc + '">' + threat.risk + ' Risk</span><a href="https://atlas.mitre.org/techniques/' + threat.atlas + '" target="_blank" class="detail-badge atlas-link" style="background:rgba(255,90,54,0.08)">' + threat.atlas + '</a>';
755-
document.getElementById('detailBody').innerHTML = '<div class="detail-section"><div class="detail-label">Description</div><div class="detail-value">' + threat.description + '</div></div><div class="detail-section"><div class="detail-label">Attack Vector</div><div class="detail-value">' + threat.attackVector + '</div></div><div class="detail-section"><div class="detail-label">Affected Components</div><div class="detail-value">' + threat.affected + '</div></div><div class="detail-section"><div class="detail-label">Current Mitigations</div><div class="detail-value">' + threat.mitigations + '</div></div><div class="detail-section"><div class="detail-label">Residual Risk</div><div class="detail-value">' + threat.residualRisk + '</div></div><div class="detail-section"><div class="detail-label">Recommendations</div><div class="detail-value">' + threat.recommendations + '</div></div>';
757+
document.getElementById('detailBadges').innerHTML = '<span class="detail-badge" style="background:' + rc + '12;color:' + rc + '">' + escapeHtml(threat.risk) + ' Risk</span><a href="https://atlas.mitre.org/techniques/' + escapeHtml(threat.atlas) + '" target="_blank" class="detail-badge atlas-link" style="background:rgba(255,90,54,0.08)">' + escapeHtml(threat.atlas) + '</a>';
758+
document.getElementById('detailBody').innerHTML = '<div class="detail-section"><div class="detail-label">Description</div><div class="detail-value">' + escapeHtml(threat.description) + '</div></div><div class="detail-section"><div class="detail-label">Attack Vector</div><div class="detail-value">' + escapeHtml(threat.attackVector) + '</div></div><div class="detail-section"><div class="detail-label">Affected Components</div><div class="detail-value">' + escapeHtml(threat.affected) + '</div></div><div class="detail-section"><div class="detail-label">Current Mitigations</div><div class="detail-value">' + escapeHtml(threat.mitigations) + '</div></div><div class="detail-section"><div class="detail-label">Residual Risk</div><div class="detail-value">' + escapeHtml(threat.residualRisk) + '</div></div><div class="detail-section"><div class="detail-label">Recommendations</div><div class="detail-value">' + escapeHtml(threat.recommendations) + '</div></div>';
756759
document.getElementById('detailOverlay').classList.add('active');
757760
document.querySelectorAll('.threat-card').forEach(function(c) { c.classList.remove('selected'); });
758761
var sel = document.querySelector('[data-threat-id="' + threatId + '"]');

src/pages/trust/zh-cn/threatmodel.astro

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -746,14 +746,17 @@ for (const b of trustBoundaries) {
746746
function getRiskColor(risk) {
747747
switch (risk) { case 'Critical': return '#ef4444'; case 'High': return '#f97316'; case 'Medium': return '#eab308'; case 'Low': return '#22c55e'; default: return '#6b7280'; }
748748
}
749+
function escapeHtml(s) {
750+
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
751+
}
749752
function showThreatDetail(threatId) {
750753
var threat = threats.find(function(t) { return t.id === threatId; });
751754
if (!threat) return;
752755
document.getElementById('detailId').textContent = threat.id + ' \u2022 ' + threat.tactic;
753756
document.getElementById('detailTitle').textContent = threat.name;
754757
var rc = getRiskColor(threat.risk);
755-
document.getElementById('detailBadges').innerHTML = '<span class="detail-badge" style="background:' + rc + '12;color:' + rc + '">' + threat.risk + ' ' + detailLabels.riskSuffix + '</span><a href="https://atlas.mitre.org/techniques/' + threat.atlas + '" target="_blank" class="detail-badge atlas-link" style="background:rgba(255,90,54,0.08)">' + threat.atlas + '</a>';
756-
document.getElementById('detailBody').innerHTML = '<div class="detail-section"><div class="detail-label">' + detailLabels.description + '</div><div class="detail-value">' + threat.description + '</div></div><div class="detail-section"><div class="detail-label">' + detailLabels.attackVector + '</div><div class="detail-value">' + threat.attackVector + '</div></div><div class="detail-section"><div class="detail-label">' + detailLabels.affected + '</div><div class="detail-value">' + threat.affected + '</div></div><div class="detail-section"><div class="detail-label">' + detailLabels.mitigations + '</div><div class="detail-value">' + threat.mitigations + '</div></div><div class="detail-section"><div class="detail-label">' + detailLabels.residualRisk + '</div><div class="detail-value">' + threat.residualRisk + '</div></div><div class="detail-section"><div class="detail-label">' + detailLabels.recommendations + '</div><div class="detail-value">' + threat.recommendations + '</div></div>';
758+
document.getElementById('detailBadges').innerHTML = '<span class="detail-badge" style="background:' + rc + '12;color:' + rc + '">' + escapeHtml(threat.risk) + ' ' + escapeHtml(detailLabels.riskSuffix) + '</span><a href="https://atlas.mitre.org/techniques/' + escapeHtml(threat.atlas) + '" target="_blank" class="detail-badge atlas-link" style="background:rgba(255,90,54,0.08)">' + escapeHtml(threat.atlas) + '</a>';
759+
document.getElementById('detailBody').innerHTML = '<div class="detail-section"><div class="detail-label">' + escapeHtml(detailLabels.description) + '</div><div class="detail-value">' + escapeHtml(threat.description) + '</div></div><div class="detail-section"><div class="detail-label">' + escapeHtml(detailLabels.attackVector) + '</div><div class="detail-value">' + escapeHtml(threat.attackVector) + '</div></div><div class="detail-section"><div class="detail-label">' + escapeHtml(detailLabels.affected) + '</div><div class="detail-value">' + escapeHtml(threat.affected) + '</div></div><div class="detail-section"><div class="detail-label">' + escapeHtml(detailLabels.mitigations) + '</div><div class="detail-value">' + escapeHtml(threat.mitigations) + '</div></div><div class="detail-section"><div class="detail-label">' + escapeHtml(detailLabels.residualRisk) + '</div><div class="detail-value">' + escapeHtml(threat.residualRisk) + '</div></div><div class="detail-section"><div class="detail-label">' + escapeHtml(detailLabels.recommendations) + '</div><div class="detail-value">' + escapeHtml(threat.recommendations) + '</div></div>';
757760
document.getElementById('detailOverlay').classList.add('active');
758761
document.querySelectorAll('.threat-card').forEach(function(c) { c.classList.remove('selected'); });
759762
var sel = document.querySelector('[data-threat-id="' + threatId + '"]');

0 commit comments

Comments
 (0)