Skip to content

Commit dd5c781

Browse files
SebTardifsteipete
andauthored
fix: escape YAML data in threat model detail overlay to prevent XSS (#140)
* 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> * fix: use DOM API for badges to prevent href attribute XSS Replace innerHTML badge construction with createElement/setAttribute to address VADE security review feedback. escapeHtml() is insufficient for href attributes because the browser decodes HTML entities during innerHTML parsing, allowing attribute boundary breakout. Using setAttribute bypasses HTML parsing entirely. All four locale files updated (en, ja, ko, zh-cn). Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca> * fix: harden threat model overlay rendering --------- Signed-off-by: Sebastien Tardif <sebtardif@ncf.ca> Co-authored-by: Peter Steinberger <steipete@gmail.com>
1 parent f69ef87 commit dd5c781

4 files changed

Lines changed: 148 additions & 12 deletions

File tree

src/pages/trust/ja/threatmodel.astro

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -746,17 +746,51 @@ 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 appendDetailSection(parent, label, value) {
750+
var section = document.createElement('div');
751+
section.className = 'detail-section';
752+
var labelDiv = document.createElement('div');
753+
labelDiv.className = 'detail-label';
754+
labelDiv.textContent = label;
755+
var valueDiv = document.createElement('div');
756+
valueDiv.className = 'detail-value';
757+
valueDiv.textContent = value == null ? '' : String(value);
758+
section.appendChild(labelDiv);
759+
section.appendChild(valueDiv);
760+
parent.appendChild(section);
761+
}
749762
function showThreatDetail(threatId) {
750763
var threat = threats.find(function(t) { return t.id === threatId; });
751764
if (!threat) return;
752765
document.getElementById('detailId').textContent = threat.id + ' \u2022 ' + threat.tactic;
753766
document.getElementById('detailTitle').textContent = threat.name;
754767
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>';
768+
var badgesDiv = document.getElementById('detailBadges');
769+
badgesDiv.textContent = '';
770+
var riskSpan = document.createElement('span');
771+
riskSpan.className = 'detail-badge';
772+
riskSpan.style.background = rc + '12';
773+
riskSpan.style.color = rc;
774+
riskSpan.textContent = threat.risk + ' ' + detailLabels.riskSuffix;
775+
badgesDiv.appendChild(riskSpan);
776+
var atlasLink = document.createElement('a');
777+
atlasLink.className = 'detail-badge atlas-link';
778+
atlasLink.style.background = 'rgba(255,90,54,0.08)';
779+
atlasLink.setAttribute('href', 'https://atlas.mitre.org/techniques/' + threat.atlas);
780+
atlasLink.setAttribute('target', '_blank');
781+
atlasLink.textContent = threat.atlas;
782+
badgesDiv.appendChild(atlasLink);
783+
var detailBody = document.getElementById('detailBody');
784+
detailBody.textContent = '';
785+
appendDetailSection(detailBody, detailLabels.description, threat.description);
786+
appendDetailSection(detailBody, detailLabels.attackVector, threat.attackVector);
787+
appendDetailSection(detailBody, detailLabels.affected, threat.affected);
788+
appendDetailSection(detailBody, detailLabels.mitigations, threat.mitigations);
789+
appendDetailSection(detailBody, detailLabels.residualRisk, threat.residualRisk);
790+
appendDetailSection(detailBody, detailLabels.recommendations, threat.recommendations);
757791
document.getElementById('detailOverlay').classList.add('active');
758792
document.querySelectorAll('.threat-card').forEach(function(c) { c.classList.remove('selected'); });
759-
var sel = document.querySelector('[data-threat-id="' + threatId + '"]');
793+
var sel = Array.prototype.find.call(document.querySelectorAll('.threat-card'), function(c) { return c.dataset.threatId === threatId; });
760794
if (sel) sel.classList.add('selected');
761795
}
762796
function closeDetail() { document.getElementById('detailOverlay').classList.remove('active'); document.querySelectorAll('.threat-card').forEach(function(c) { c.classList.remove('selected'); }); }

src/pages/trust/ko/threatmodel.astro

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -746,17 +746,51 @@ 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 appendDetailSection(parent, label, value) {
750+
var section = document.createElement('div');
751+
section.className = 'detail-section';
752+
var labelDiv = document.createElement('div');
753+
labelDiv.className = 'detail-label';
754+
labelDiv.textContent = label;
755+
var valueDiv = document.createElement('div');
756+
valueDiv.className = 'detail-value';
757+
valueDiv.textContent = value == null ? '' : String(value);
758+
section.appendChild(labelDiv);
759+
section.appendChild(valueDiv);
760+
parent.appendChild(section);
761+
}
749762
function showThreatDetail(threatId) {
750763
var threat = threats.find(function(t) { return t.id === threatId; });
751764
if (!threat) return;
752765
document.getElementById('detailId').textContent = threat.id + ' \u2022 ' + threat.tactic;
753766
document.getElementById('detailTitle').textContent = threat.name;
754767
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>';
768+
var badgesDiv = document.getElementById('detailBadges');
769+
badgesDiv.textContent = '';
770+
var riskSpan = document.createElement('span');
771+
riskSpan.className = 'detail-badge';
772+
riskSpan.style.background = rc + '12';
773+
riskSpan.style.color = rc;
774+
riskSpan.textContent = threat.risk + ' ' + detailLabels.riskSuffix;
775+
badgesDiv.appendChild(riskSpan);
776+
var atlasLink = document.createElement('a');
777+
atlasLink.className = 'detail-badge atlas-link';
778+
atlasLink.style.background = 'rgba(255,90,54,0.08)';
779+
atlasLink.setAttribute('href', 'https://atlas.mitre.org/techniques/' + threat.atlas);
780+
atlasLink.setAttribute('target', '_blank');
781+
atlasLink.textContent = threat.atlas;
782+
badgesDiv.appendChild(atlasLink);
783+
var detailBody = document.getElementById('detailBody');
784+
detailBody.textContent = '';
785+
appendDetailSection(detailBody, detailLabels.description, threat.description);
786+
appendDetailSection(detailBody, detailLabels.attackVector, threat.attackVector);
787+
appendDetailSection(detailBody, detailLabels.affected, threat.affected);
788+
appendDetailSection(detailBody, detailLabels.mitigations, threat.mitigations);
789+
appendDetailSection(detailBody, detailLabels.residualRisk, threat.residualRisk);
790+
appendDetailSection(detailBody, detailLabels.recommendations, threat.recommendations);
757791
document.getElementById('detailOverlay').classList.add('active');
758792
document.querySelectorAll('.threat-card').forEach(function(c) { c.classList.remove('selected'); });
759-
var sel = document.querySelector('[data-threat-id="' + threatId + '"]');
793+
var sel = Array.prototype.find.call(document.querySelectorAll('.threat-card'), function(c) { return c.dataset.threatId === threatId; });
760794
if (sel) sel.classList.add('selected');
761795
}
762796
function closeDetail() { document.getElementById('detailOverlay').classList.remove('active'); document.querySelectorAll('.threat-card').forEach(function(c) { c.classList.remove('selected'); }); }

src/pages/trust/threatmodel.astro

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -745,17 +745,51 @@ 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 appendDetailSection(parent, label, value) {
749+
var section = document.createElement('div');
750+
section.className = 'detail-section';
751+
var labelDiv = document.createElement('div');
752+
labelDiv.className = 'detail-label';
753+
labelDiv.textContent = label;
754+
var valueDiv = document.createElement('div');
755+
valueDiv.className = 'detail-value';
756+
valueDiv.textContent = value == null ? '' : String(value);
757+
section.appendChild(labelDiv);
758+
section.appendChild(valueDiv);
759+
parent.appendChild(section);
760+
}
748761
function showThreatDetail(threatId) {
749762
var threat = threats.find(function(t) { return t.id === threatId; });
750763
if (!threat) return;
751764
document.getElementById('detailId').textContent = threat.id + ' \u2022 ' + threat.tactic;
752765
document.getElementById('detailTitle').textContent = threat.name;
753766
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>';
767+
var badgesDiv = document.getElementById('detailBadges');
768+
badgesDiv.textContent = '';
769+
var riskSpan = document.createElement('span');
770+
riskSpan.className = 'detail-badge';
771+
riskSpan.style.background = rc + '12';
772+
riskSpan.style.color = rc;
773+
riskSpan.textContent = threat.risk + ' Risk';
774+
badgesDiv.appendChild(riskSpan);
775+
var atlasLink = document.createElement('a');
776+
atlasLink.className = 'detail-badge atlas-link';
777+
atlasLink.style.background = 'rgba(255,90,54,0.08)';
778+
atlasLink.setAttribute('href', 'https://atlas.mitre.org/techniques/' + threat.atlas);
779+
atlasLink.setAttribute('target', '_blank');
780+
atlasLink.textContent = threat.atlas;
781+
badgesDiv.appendChild(atlasLink);
782+
var detailBody = document.getElementById('detailBody');
783+
detailBody.textContent = '';
784+
appendDetailSection(detailBody, 'Description', threat.description);
785+
appendDetailSection(detailBody, 'Attack Vector', threat.attackVector);
786+
appendDetailSection(detailBody, 'Affected Components', threat.affected);
787+
appendDetailSection(detailBody, 'Current Mitigations', threat.mitigations);
788+
appendDetailSection(detailBody, 'Residual Risk', threat.residualRisk);
789+
appendDetailSection(detailBody, 'Recommendations', threat.recommendations);
756790
document.getElementById('detailOverlay').classList.add('active');
757791
document.querySelectorAll('.threat-card').forEach(function(c) { c.classList.remove('selected'); });
758-
var sel = document.querySelector('[data-threat-id="' + threatId + '"]');
792+
var sel = Array.prototype.find.call(document.querySelectorAll('.threat-card'), function(c) { return c.dataset.threatId === threatId; });
759793
if (sel) sel.classList.add('selected');
760794
}
761795
function closeDetail() { document.getElementById('detailOverlay').classList.remove('active'); document.querySelectorAll('.threat-card').forEach(function(c) { c.classList.remove('selected'); }); }

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

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -746,17 +746,51 @@ 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 appendDetailSection(parent, label, value) {
750+
var section = document.createElement('div');
751+
section.className = 'detail-section';
752+
var labelDiv = document.createElement('div');
753+
labelDiv.className = 'detail-label';
754+
labelDiv.textContent = label;
755+
var valueDiv = document.createElement('div');
756+
valueDiv.className = 'detail-value';
757+
valueDiv.textContent = value == null ? '' : String(value);
758+
section.appendChild(labelDiv);
759+
section.appendChild(valueDiv);
760+
parent.appendChild(section);
761+
}
749762
function showThreatDetail(threatId) {
750763
var threat = threats.find(function(t) { return t.id === threatId; });
751764
if (!threat) return;
752765
document.getElementById('detailId').textContent = threat.id + ' \u2022 ' + threat.tactic;
753766
document.getElementById('detailTitle').textContent = threat.name;
754767
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>';
768+
var badgesDiv = document.getElementById('detailBadges');
769+
badgesDiv.textContent = '';
770+
var riskSpan = document.createElement('span');
771+
riskSpan.className = 'detail-badge';
772+
riskSpan.style.background = rc + '12';
773+
riskSpan.style.color = rc;
774+
riskSpan.textContent = threat.risk + ' ' + detailLabels.riskSuffix;
775+
badgesDiv.appendChild(riskSpan);
776+
var atlasLink = document.createElement('a');
777+
atlasLink.className = 'detail-badge atlas-link';
778+
atlasLink.style.background = 'rgba(255,90,54,0.08)';
779+
atlasLink.setAttribute('href', 'https://atlas.mitre.org/techniques/' + threat.atlas);
780+
atlasLink.setAttribute('target', '_blank');
781+
atlasLink.textContent = threat.atlas;
782+
badgesDiv.appendChild(atlasLink);
783+
var detailBody = document.getElementById('detailBody');
784+
detailBody.textContent = '';
785+
appendDetailSection(detailBody, detailLabels.description, threat.description);
786+
appendDetailSection(detailBody, detailLabels.attackVector, threat.attackVector);
787+
appendDetailSection(detailBody, detailLabels.affected, threat.affected);
788+
appendDetailSection(detailBody, detailLabels.mitigations, threat.mitigations);
789+
appendDetailSection(detailBody, detailLabels.residualRisk, threat.residualRisk);
790+
appendDetailSection(detailBody, detailLabels.recommendations, threat.recommendations);
757791
document.getElementById('detailOverlay').classList.add('active');
758792
document.querySelectorAll('.threat-card').forEach(function(c) { c.classList.remove('selected'); });
759-
var sel = document.querySelector('[data-threat-id="' + threatId + '"]');
793+
var sel = Array.prototype.find.call(document.querySelectorAll('.threat-card'), function(c) { return c.dataset.threatId === threatId; });
760794
if (sel) sel.classList.add('selected');
761795
}
762796
function closeDetail() { document.getElementById('detailOverlay').classList.remove('active'); document.querySelectorAll('.threat-card').forEach(function(c) { c.classList.remove('selected'); }); }

0 commit comments

Comments
 (0)