Skip to content

Commit 8925026

Browse files
committed
security: Prevent DOM-based XSS in visualization viewer
- Add escapeHtml() function to sanitize all user data before DOM insertion - Apply HTML escaping to tooltips, info panels, tables, and charts - Protect against XSS via CSV field injection (region_id, classification, etc) - Secure innerHTML and template literal usage throughout visualization tool All data rendered in HTML is now properly escaped
1 parent 07fe889 commit 8925026

2 files changed

Lines changed: 27 additions & 16 deletions

File tree

0 Bytes
Binary file not shown.

src/pyehsa/visualization/ehsa_visualization.html

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,17 @@ <h5>Time Series Analysis</h5>
201201
let neighborFeatures = [];
202202
let timeSeriesChart = null;
203203

204+
// Security: Escape HTML to prevent XSS attacks
205+
function escapeHtml(unsafe) {
206+
if (unsafe == null || unsafe === undefined) return '';
207+
return String(unsafe)
208+
.replace(/&/g, "&amp;")
209+
.replace(/</g, "&lt;")
210+
.replace(/>/g, "&gt;")
211+
.replace(/"/g, "&quot;")
212+
.replace(/'/g, "&#039;");
213+
}
214+
204215
// Atualizar as cores das classificações conforme o gabarito fornecido
205216
const classificationColors = {
206217
'no pattern detected': 'lightgray',
@@ -526,8 +537,8 @@ <h5>Time Series Analysis</h5>
526537

527538
layer.bindTooltip(`
528539
<div class="tooltip-custom">
529-
<h6>Region: ${regionId}</h6>
530-
<p>Classification: ${classification}</p>
540+
<h6>Region: ${escapeHtml(regionId)}</h6>
541+
<p>Classification: ${escapeHtml(classification)}</p>
531542
</div>
532543
`, {
533544
sticky: true,
@@ -659,11 +670,11 @@ <h6>Region: ${regionId}</h6>
659670
// Build an HTML table
660671
let cols = Object.keys(parsedData[0]);
661672
let html = '<div style="overflow-x:auto;"><table border="1" cellpadding="4" cellspacing="0" style="border-collapse:collapse;font-size:12px;width:100%"><thead><tr>';
662-
cols.forEach(col => { html += `<th>${col}</th>`; });
673+
cols.forEach(col => { html += `<th>${escapeHtml(col)}</th>`; });
663674
html += '</tr></thead><tbody>';
664675
parsedData.forEach(row => {
665676
html += '<tr>';
666-
cols.forEach(col => { html += `<td>${row[col] !== null && row[col] !== undefined ? row[col] : ''}</td>`; });
677+
cols.forEach(col => { html += `<td>${escapeHtml(row[col] !== null && row[col] !== undefined ? row[col] : '')}</td>`; });
667678
html += '</tr>';
668679
});
669680
html += '</tbody></table></div>';
@@ -682,7 +693,7 @@ <h6>Region: ${regionId}</h6>
682693
} else if (typeof data === 'object' && data !== null) {
683694
html += '<ul style="margin:0 0 0 1em;padding:0">';
684695
for (const k in data) {
685-
html += `<li>${pad}<strong>${k}:</strong> ${prettyPrintData(data[k], indent + 1, k)}</li>`;
696+
html += `<li>${pad}<strong>${escapeHtml(k)}:</strong> ${prettyPrintData(data[k], indent + 1, k)}</li>`;
686697
}
687698
html += '</ul>';
688699
} else {
@@ -691,7 +702,7 @@ <h6>Region: ${regionId}</h6>
691702
if (typeof parsedData === 'object' && parsedData !== null) {
692703
return prettyPrintData(parsedData, indent + 1);
693704
}
694-
html += pad + String(data);
705+
html += pad + escapeHtml(String(data));
695706
}
696707
return html;
697708
}
@@ -703,10 +714,10 @@ <h6>Region: ${regionId}</h6>
703714
const classification = feature.properties.classification;
704715

705716
let html = `
706-
<h6>Region: ${regionId}</h6>
707-
<p><strong>Classification:</strong> ${classification}</p>
708-
<p><strong>Mann-Kendall Tau:</strong> ${feature.properties.tau}</p>
709-
<p><strong>p-value:</strong> ${feature.properties.p_value}</p>
717+
<h6>Region: ${escapeHtml(regionId)}</h6>
718+
<p><strong>Classification:</strong> ${escapeHtml(classification)}</p>
719+
<p><strong>Mann-Kendall Tau:</strong> ${escapeHtml(feature.properties.tau)}</p>
720+
<p><strong>p-value:</strong> ${escapeHtml(feature.properties.p_value)}</p>
710721
`;
711722

712723
// Show all details recursively
@@ -996,7 +1007,7 @@ <h6>Region: ${regionId}</h6>
9961007
plugins: {
9971008
title: {
9981009
display: true,
999-
text: `Time Series for Region: ${getRegionId(feature.properties)}`
1010+
text: `Time Series for Region: ${escapeHtml(getRegionId(feature.properties))}`
10001011
},
10011012
tooltip: {
10021013
callbacks: {
@@ -1030,11 +1041,11 @@ <h6>Region: ${regionId}</h6>
10301041
const tau = feature.properties.tau;
10311042
const pValue = feature.properties.p_value;
10321043
timeSeriesInfo.innerHTML = `
1033-
<h5>Time Series Analysis for Region: ${getRegionId(feature.properties)}</h5>
1034-
<p><strong>Classification:</strong> ${classification}</p>
1035-
<p><strong>Mann-Kendall Tau:</strong> ${tau} (p-value: ${pValue})</p>
1036-
<p><strong>Number of Time Periods:</strong> ${data.length}</p>
1037-
<p><strong>Significant Periods:</strong> ${data.filter(d => d.is_significant).length}</p>
1044+
<h5>Time Series Analysis for Region: ${escapeHtml(getRegionId(feature.properties))}</h5>
1045+
<p><strong>Classification:</strong> ${escapeHtml(classification)}</p>
1046+
<p><strong>Mann-Kendall Tau:</strong> ${escapeHtml(tau)} (p-value: ${escapeHtml(pValue)})</p>
1047+
<p><strong>Number of Time Periods:</strong> ${escapeHtml(data.length)}</p>
1048+
<p><strong>Significant Periods:</strong> ${escapeHtml(data.filter(d => d.is_significant).length)}</p>
10381049
`;
10391050
});
10401051

0 commit comments

Comments
 (0)