fix(security): replace innerHTML with DOM API to prevent XSS in common.js

- Replace innerHTML assignments with createElement/textContent to prevent XSS
- Properly escape user data (display name, picture URL) from OAuth responses
- Remove stale 'files copy.html' template file

Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-02-13 11:45:29 +00:00
parent f9c3c1582b
commit 7524819c77
2 changed files with 79 additions and 214 deletions
+79 -29
View File
@@ -16,56 +16,106 @@
// Get the display name (prefer name, fall back to preferred_username, then email)
const displayName = data.name || data.preferred_username || data.email;
// User is logged in
let authHTML = `
<div class="flex items-center">
<img src="${data.picture}" alt="Avatar" class="w-8 h-8 rounded-full mr-2" />
<span>${displayName}</span>
<a href="/logout" class="ml-3 text-red-600 hover:text-red-800">
<i class="fas fa-sign-out-alt"></i>
</a>
</div>
`;
// User is logged in - use DOM API to prevent XSS
if (authSection) {
authSection.innerHTML = authHTML;
authSection.textContent = ''; // Clear existing content
const container = document.createElement('div');
container.className = 'flex items-center';
const img = document.createElement('img');
img.src = data.picture;
img.alt = 'Avatar';
img.className = 'w-8 h-8 rounded-full mr-2';
const span = document.createElement('span');
span.textContent = displayName;
const logoutLink = document.createElement('a');
logoutLink.href = '/logout';
logoutLink.className = 'ml-3 text-red-600 hover:text-red-800';
const icon = document.createElement('i');
icon.className = 'fas fa-sign-out-alt';
logoutLink.appendChild(icon);
container.appendChild(img);
container.appendChild(span);
container.appendChild(logoutLink);
authSection.appendChild(container);
}
if (mobileAuthSection) {
mobileAuthSection.innerHTML = `
<div class="flex items-center justify-between">
<div class="flex items-center">
<img src="${data.picture}" alt="Avatar" class="w-6 h-6 rounded-full mr-2" />
<span>${displayName}</span>
</div>
<a href="/logout" class="text-red-600 hover:text-red-800">
<i class="fas fa-sign-out-alt"></i> Logout
</a>
</div>
`;
mobileAuthSection.textContent = ''; // Clear existing content
const outerContainer = document.createElement('div');
outerContainer.className = 'flex items-center justify-between';
const innerContainer = document.createElement('div');
innerContainer.className = 'flex items-center';
const img = document.createElement('img');
img.src = data.picture;
img.alt = 'Avatar';
img.className = 'w-6 h-6 rounded-full mr-2';
const span = document.createElement('span');
span.textContent = displayName;
innerContainer.appendChild(img);
innerContainer.appendChild(span);
const logoutLink = document.createElement('a');
logoutLink.href = '/logout';
logoutLink.className = 'text-red-600 hover:text-red-800';
const icon = document.createElement('i');
icon.className = 'fas fa-sign-out-alt';
logoutLink.appendChild(icon);
logoutLink.appendChild(document.createTextNode(' Logout'));
outerContainer.appendChild(innerContainer);
outerContainer.appendChild(logoutLink);
mobileAuthSection.appendChild(outerContainer);
}
} else {
// User is not logged in (this shouldn't happen with current setup, but keeping as fallback)
// User is not logged in - use DOM API
if (authSection) {
authSection.innerHTML = `<a href="/login" class="text-blue-600">Login</a>`;
authSection.textContent = '';
const loginLink = document.createElement('a');
loginLink.href = '/login';
loginLink.className = 'text-blue-600';
loginLink.textContent = 'Login';
authSection.appendChild(loginLink);
}
if (mobileAuthSection) {
mobileAuthSection.innerHTML = `<a href="/login" class="text-blue-600">Login</a>`;
mobileAuthSection.textContent = '';
const loginLink = document.createElement('a');
loginLink.href = '/login';
loginLink.className = 'text-blue-600';
loginLink.textContent = 'Login';
mobileAuthSection.appendChild(loginLink);
}
}
} catch (error) {
console.error('Authentication check failed:', error);
// Fallback if whoami endpoint fails
// Fallback if whoami endpoint fails - use DOM API
const authSection = document.getElementById("authSection");
const mobileAuthSection = document.getElementById("mobileAuthSection");
if (authSection) {
authSection.innerHTML = `<a href="/login" class="text-blue-600">Login</a>`;
authSection.textContent = '';
const loginLink = document.createElement('a');
loginLink.href = '/login';
loginLink.className = 'text-blue-600';
loginLink.textContent = 'Login';
authSection.appendChild(loginLink);
}
if (mobileAuthSection) {
mobileAuthSection.innerHTML = `<a href="/login" class="text-blue-600">Login</a>`;
mobileAuthSection.textContent = '';
const loginLink = document.createElement('a');
loginLink.href = '/login';
loginLink.className = 'text-blue-600';
loginLink.textContent = 'Login';
mobileAuthSection.appendChild(loginLink);
}
}
})();
-185
View File
@@ -1,185 +0,0 @@
{% extends "base.html" %}
{% block title %}File Records{% endblock %}
{% block head_extra %}
<!-- Include Grid.js CSS -->
<link href="https://unpkg.com/gridjs/dist/theme/mermaid.min.css" rel="stylesheet" />
<script src="/static/js/common.js"></script>
<!-- Make sure Alpine.js is properly initialized -->
<script>
console.log('Before Alpine init on /files page');
document.addEventListener('alpine:init', () => {
console.log('Alpine.js initialized in files view');
});
console.log('After Alpine init listener registration');
</script>
<style>
.delete-btn {
color: #e53e3e;
cursor: pointer;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
display: flex;
justify-content: center;
align-items: center;
}
.delete-btn:hover {
background-color: #fed7d7;
}
.confirm-delete-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.confirm-delete-content {
background-color: white;
padding: 1.5rem;
border-radius: 0.5rem;
max-width: 500px;
width: 90%;
}
.hidden {
display: none;
}
</style>
{% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 py-8">
<h2 class="text-3xl font-bold mb-6">File Records</h2>
<!-- Grid.js will render the table in this container -->
<div id="gridjs-wrapper"></div>
<!-- Confirmation Modal (temporarily disabled) -->
<div id="confirmDeleteModal" class="confirm-delete-modal hidden">
<div class="confirm-delete-content">
<h3 class="text-lg font-medium text-gray-900 mb-4">Confirm Deletion</h3>
<p class="text-sm text-gray-500 mb-4">
Are you sure you want to delete this file record? This action cannot be undone.
</p>
<p class="text-sm text-gray-700 mb-4">
<strong>File:</strong> <span id="deleteFileName"></span>
</p>
<div class="flex justify-end space-x-3">
<button id="cancelDelete" class="px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300">
Cancel
</button>
<button id="confirmDelete" class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700">
Delete
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<!-- Include Grid.js JS -->
<script src="https://unpkg.com/gridjs/dist/gridjs.umd.js"></script>
<script>
// File deletion handling
let fileToDelete = null;
const confirmDeleteModal = document.getElementById('confirmDeleteModal');
const deleteFileName = document.getElementById('deleteFileName');
const cancelDelete = document.getElementById('cancelDelete');
const confirmDelete = document.getElementById('confirmDelete');
// Close modal
function closeDeleteModal() {
confirmDeleteModal.classList.add('hidden');
fileToDelete = null;
}
// Delete file directly without confirmation
async function deleteFile(fileId) {
try {
const response = await fetch(`/api/files/${fileId}`, {
method: 'DELETE'
});
if (response.ok) {
// Success - reload the grid
grid.forceRender();
} else {
// Show error
const error = await response.json();
alert(`Error: ${error.detail || 'Failed to delete file'}`);
}
} catch (err) {
console.error('Delete error:', err);
alert('An error occurred while deleting the file');
}
}
// Show delete confirmation modal (temporarily bypassed)
function showDeleteModal(fileId, fileName) {
// Bypass modal and delete immediately
deleteFile(fileId);
// The original code is commented out:
// fileToDelete = { id: fileId, name: fileName };
// deleteFileName.textContent = fileName || `ID: ${fileId}`;
// confirmDeleteModal.classList.remove('hidden');
}
// Handle delete confirmation
confirmDelete.addEventListener('click', async function() {
if (!fileToDelete) return;
deleteFile(fileToDelete.id);
closeDeleteModal();
});
// Cancel delete
cancelDelete.addEventListener('click', closeDeleteModal);
confirmDeleteModal.addEventListener('click', function(e) {
if (e.target === confirmDeleteModal) closeDeleteModal();
});
// Initialize the grid
const grid = new gridjs.Grid({
columns: [
{ id: 'id', name: 'ID' },
{ id: 'original_filename', name: 'Original Filename' },
{ id: 'file_size', name: 'File Size', formatter: (size) => `${(size / 1024).toFixed(2)} KB` },
{ id: 'mime_type', name: 'Mime Type' },
{ id: 'created_at', name: 'Created At' },
{
id: 'actions',
name: 'Actions',
formatter: (_, row) => {
return gridjs.h('button', {
className: 'delete-btn',
onClick: () => showDeleteModal(row.cells[0].data, row.cells[1].data)
}, [
gridjs.h('i', { className: 'fas fa-trash' }),
]);
}
}
],
server: {
url: '/api/files',
then: data => data.map(file => [
file.id,
file.original_filename || "",
file.file_size,
file.mime_type,
file.created_at || "",
"" // This cell will be rendered by the formatter
])
},
search: true,
sort: true,
pagination: {
limit: 10
}
}).render(document.getElementById("gridjs-wrapper"));
</script>
{% endblock %}