diff --git a/internal/server/browse_test.go b/internal/server/browse_test.go index ed7af57..b85116d 100644 --- a/internal/server/browse_test.go +++ b/internal/server/browse_test.go @@ -375,6 +375,19 @@ func TestHandleBrowseSourcePage(t *testing.T) { } } + // Check that the escapeHTML function is present for XSS protection + if !strings.Contains(body, "function escapeHTML(str)") { + t.Error("browse source page missing escapeHTML function for XSS protection") + } + + // Check that onclick handlers use escapeHTML + if strings.Contains(body, "onclick=\"loadFileTree('${file.path}')") { + t.Error("browse source page has unescaped file.path in onclick handler") + } + if strings.Contains(body, "onclick=\"loadFile('${file.path}')") { + t.Error("browse source page has unescaped file.path in onclick handler") + } + // Check that ecosystem, package name, and version are set in JavaScript if !strings.Contains(body, "const ecosystem = 'npm'") { t.Error("browse source page missing ecosystem variable") diff --git a/internal/server/templates/pages/browse_source.html b/internal/server/templates/pages/browse_source.html index f7a08dc..710da1c 100644 --- a/internal/server/templates/pages/browse_source.html +++ b/internal/server/templates/pages/browse_source.html @@ -54,6 +54,14 @@

Select a file

const version = '{{.Version}}'; let currentPath = ''; +// Escape a string for safe interpolation into HTML attributes and content. +// Prevents XSS when file paths contain quotes, angle brackets, or other special characters. +function escapeHTML(str) { + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML.replace(/'/g, ''').replace(/"/g, '"'); +} + // Load file tree for a directory async function loadFileTree(path = '') { try { @@ -93,7 +101,7 @@

Select a file

const parentPath = basePath.split('/').slice(0, -2).join('/'); html += `
+ onclick="loadFileTree('${escapeHTML(parentPath)}'); currentPath='${escapeHTML(parentPath)}';"> 📁 ..
`; @@ -106,14 +114,14 @@

Select a file

if (file.is_dir) { html += ` -
- ${icon} ${file.name} +
+ ${icon} ${escapeHTML(file.name)}
`; } else { html += ` -
- ${icon} ${file.name} +
+ ${icon} ${escapeHTML(file.name)} ${formatSize(file.size)}
`; @@ -150,7 +158,7 @@

Select a file

const container = document.getElementById('file-content'); if (contentType && contentType.includes('image/')) { - container.innerHTML = `${path}`; + container.innerHTML = `${escapeHTML(path)}`; } else if (isTextContent(contentType)) { // Escape HTML and display as code const escaped = content @@ -165,7 +173,7 @@

Select a file

container.innerHTML = `

Binary file (${formatSize(content.length)})

-