Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions internal/server/browse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
22 changes: 15 additions & 7 deletions internal/server/templates/pages/browse_source.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ <h2 class="font-semibold font-mono text-sm" id="file-path">Select a file</h2>
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, '&#39;').replace(/"/g, '&quot;');
}

// Load file tree for a directory
async function loadFileTree(path = '') {
try {
Expand Down Expand Up @@ -93,7 +101,7 @@ <h2 class="font-semibold font-mono text-sm" id="file-path">Select a file</h2>
const parentPath = basePath.split('/').slice(0, -2).join('/');
html += `
<div class="px-2 py-1 hover:bg-gray-100 dark:hover:bg-gray-800 rounded cursor-pointer text-sm"
onclick="loadFileTree('${parentPath}'); currentPath='${parentPath}';">
onclick="loadFileTree('${escapeHTML(parentPath)}'); currentPath='${escapeHTML(parentPath)}';">
<span class="text-gray-500 dark:text-gray-400">📁 ..</span>
</div>
`;
Expand All @@ -106,14 +114,14 @@ <h2 class="font-semibold font-mono text-sm" id="file-path">Select a file</h2>

if (file.is_dir) {
html += `
<div class="${classes}" onclick="loadFileTree('${file.path}'); currentPath='${file.path}';">
<span>${icon} ${file.name}</span>
<div class="${classes}" onclick="loadFileTree('${escapeHTML(file.path)}'); currentPath='${escapeHTML(file.path)}';">
<span>${icon} ${escapeHTML(file.name)}</span>
</div>
`;
} else {
html += `
<div class="${classes}" onclick="loadFile('${file.path}')">
<span>${icon} ${file.name}</span>
<div class="${classes}" onclick="loadFile('${escapeHTML(file.path)}')">
<span>${icon} ${escapeHTML(file.name)}</span>
<span class="text-xs text-gray-500 dark:text-gray-400 ml-2">${formatSize(file.size)}</span>
</div>
`;
Expand Down Expand Up @@ -150,7 +158,7 @@ <h2 class="font-semibold font-mono text-sm" id="file-path">Select a file</h2>
const container = document.getElementById('file-content');

if (contentType && contentType.includes('image/')) {
container.innerHTML = `<img src="${url}" alt="${path}" class="max-w-full">`;
container.innerHTML = `<img src="${escapeHTML(url)}" alt="${escapeHTML(path)}" class="max-w-full">`;
} else if (isTextContent(contentType)) {
// Escape HTML and display as code
const escaped = content
Expand All @@ -165,7 +173,7 @@ <h2 class="font-semibold font-mono text-sm" id="file-path">Select a file</h2>
container.innerHTML = `
<div class="text-center text-gray-500 dark:text-gray-400 py-8">
<p class="mb-4">Binary file (${formatSize(content.length)})</p>
<button onclick="window.location.href='${url}'"
<button onclick="window.location.href='${escapeHTML(url)}'"
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
Download
</button>
Expand Down