Merge pull request #126 from christianlouis/codex/dashboard-safe-rendering

[codex] harden dashboard domain rendering
This commit is contained in:
Christian Krakau-Louis
2026-05-23 00:20:49 +02:00
committed by GitHub
2 changed files with 91 additions and 22 deletions
+71 -22
View File
@@ -673,34 +673,83 @@ function dashboardApp() {
const tableBody = document.getElementById('domains-table-body');
if (!tableBody) return;
tableBody.innerHTML = '';
tableBody.textContent = '';
domains.forEach(domain => {
const row = document.createElement('tr');
row.className = 'table-row';
row.innerHTML = `
<td class="table-cell">
<div class="font-medium">${domain.domain_name}</div>
</td>
<td class="table-cell">${domain.total_emails || 0}</td>
<td class="table-cell">
<span class="inline-flex items-center rounded-md bg-green-50 dark:bg-green-900/20 px-2 py-1 text-xs font-medium text-green-700 dark:text-green-300">
${domain.pass_rate || 0}%
</span>
</td>
<td class="table-cell">${domain.failed_count || 0}</td>
<td class="table-cell">${domain.report_count || 0}</td>
<td class="table-cell text-right">
<a href="/domains/${domain.id}" class="btn btn-outline btn-sm">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1"><path d="M3 3v18h18"></path><path d="m19 9-5 5-4-4-3 3"></path></svg>
Details
</a>
</td>
`;
row.appendChild(this.createDomainNameCell(domain.domain_name));
row.appendChild(this.createTextCell(this.formatLargeNumber(domain.total_emails || 0)));
row.appendChild(this.createPassRateCell(domain.pass_rate || 0));
row.appendChild(this.createTextCell(this.formatLargeNumber(domain.failed_count || 0)));
row.appendChild(this.createTextCell(this.formatLargeNumber(domain.report_count || 0)));
row.appendChild(this.createDetailsCell(domain));
tableBody.appendChild(row);
});
},
createDomainNameCell(domainName) {
const cell = document.createElement('td');
cell.className = 'table-cell';
const content = document.createElement('div');
content.className = 'font-medium';
content.textContent = domainName || 'Unknown';
cell.appendChild(content);
return cell;
},
createPassRateCell(passRate) {
const cell = document.createElement('td');
cell.className = 'table-cell';
const badge = document.createElement('span');
badge.className = 'inline-flex items-center rounded-md bg-green-50 dark:bg-green-900/20 px-2 py-1 text-xs font-medium text-green-700 dark:text-green-300';
badge.textContent = `${passRate}%`;
cell.appendChild(badge);
return cell;
},
createDetailsCell(domain) {
const cell = document.createElement('td');
cell.className = 'table-cell text-right';
const link = document.createElement('a');
link.className = 'btn btn-outline btn-sm';
const domainId = String(domain.id ?? domain.domain_name ?? '');
link.href = `/domains/${encodeURIComponent(domainId)}`;
link.appendChild(this.createDetailsIcon());
link.appendChild(document.createTextNode('Details'));
cell.appendChild(link);
return cell;
},
createDetailsIcon() {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', '14');
svg.setAttribute('height', '14');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('fill', 'none');
svg.setAttribute('stroke', 'currentColor');
svg.setAttribute('stroke-width', '2');
svg.setAttribute('stroke-linecap', 'round');
svg.setAttribute('stroke-linejoin', 'round');
svg.setAttribute('class', 'mr-1');
const axis = document.createElementNS('http://www.w3.org/2000/svg', 'path');
axis.setAttribute('d', 'M3 3v18h18');
svg.appendChild(axis);
const trend = document.createElementNS('http://www.w3.org/2000/svg', 'path');
trend.setAttribute('d', 'm19 9-5 5-4-4-3 3');
svg.appendChild(trend);
return svg;
}
}
}
@@ -0,0 +1,20 @@
from pathlib import Path
def test_dashboard_domain_table_uses_safe_dom_rendering():
"""Domain names and counts come from report data and must not be HTML-rendered."""
template = (Path(__file__).resolve().parents[1] / "templates" / "index.html").read_text()
populate_start = template.index("populateDomainsTable(domains)")
helper_start = template.index("createDomainNameCell(domainName)")
populate_body = template[populate_start:helper_start]
assert "innerHTML" not in populate_body
assert ".textContent" in populate_body
assert "createDomainNameCell" in populate_body
assert "createDetailsCell" in populate_body
def test_dashboard_domain_details_links_are_encoded():
template = (Path(__file__).resolve().parents[1] / "templates" / "index.html").read_text()
assert "encodeURIComponent(domainId)" in template