From 9e97a3316a98376000760b42a9edf332db8aa18e Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Thu, 12 Mar 2026 11:59:14 +0000 Subject: [PATCH] Escape user-controlled strings in browse source JavaScript File paths from archive contents were interpolated directly into onclick handlers and innerHTML via template literals. A crafted filename containing quotes could break out of the string context and execute arbitrary JS. Add an escapeHTML helper and use it on all interpolated path and URL values in the browse source page. --- internal/server/browse_test.go | 13 +++++++++++ .../server/templates/pages/browse_source.html | 22 +++++++++++++------ 2 files changed, 28 insertions(+), 7 deletions(-) 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)})

-