fix(security): patch 3 vulnerabilities — IDOR, ownership bypass, XSS

V1: Add owner-scoped folder pagination (list_folders_by_owner_paginated)
  - New method in FolderRepository trait, PG implementation, service & handler
  - Prevents IDOR by filtering folder listings to authenticated user

V2: Enforce ownership checks on folder mutations
  - rename_folder, move_folder, delete_folder now require caller_id
  - Service verifies folder.owner_id == caller_id (returns 404 on mismatch)
  - Propagated to folder_handler, batch_handler, batch_operations, webdav_handler
  - delete_folder_with_trash upgraded from OptionalAuthUser to AuthUser
  - download_folder_zip now checks ownership before streaming

V3: Fix XSS in frontend via DOM APIs
  - sharedView.js: innerHTML → createElement + textContent
  - contextMenus.js: innerHTML → DOM construction for share dialog

Cleanup: removed unused OptionalAuthUser import, updated all stubs/mocks
This commit is contained in:
Dionisio
2026-02-16 00:22:42 +01:00
parent 66b4acd9d6
commit 5a679dfc90
25 changed files with 684 additions and 269 deletions
+38 -15
View File
@@ -543,21 +543,44 @@ const contextMenus = {
`Expires: ${window.fileSharing.formatExpirationDate(share.expires_at)}` :
'No expiration';
shareEl.innerHTML = `
<div class="share-url">${share.url}</div>
<div class="share-info">
${share.has_password ? '<span class="share-protected"><i class="fas fa-lock"></i> Password protected</span>' : ''}
<span class="share-expiration">${expiresText}</span>
</div>
<div class="share-actions">
<button class="btn btn-small copy-link-btn" data-share-url="${share.url}">
<i class="fas fa-copy"></i> Copy
</button>
<button class="btn btn-small btn-danger delete-link-btn" data-share-id="${share.id}">
<i class="fas fa-trash"></i> Delete
</button>
</div>
`;
// Share URL
const urlDiv = document.createElement('div');
urlDiv.className = 'share-url';
urlDiv.textContent = share.url;
shareEl.appendChild(urlDiv);
// Share info
const infoDiv = document.createElement('div');
infoDiv.className = 'share-info';
if (share.has_password) {
const protectedSpan = document.createElement('span');
protectedSpan.className = 'share-protected';
protectedSpan.innerHTML = '<i class="fas fa-lock"></i> Password protected';
infoDiv.appendChild(protectedSpan);
}
const expirationSpan = document.createElement('span');
expirationSpan.className = 'share-expiration';
expirationSpan.textContent = expiresText;
infoDiv.appendChild(expirationSpan);
shareEl.appendChild(infoDiv);
// Share actions
const actionsDiv = document.createElement('div');
actionsDiv.className = 'share-actions';
const copyBtn = document.createElement('button');
copyBtn.className = 'btn btn-small copy-link-btn';
copyBtn.dataset.shareUrl = share.url;
copyBtn.innerHTML = '<i class="fas fa-copy"></i> Copy';
actionsDiv.appendChild(copyBtn);
const deleteBtn = document.createElement('button');
deleteBtn.className = 'btn btn-small btn-danger delete-link-btn';
deleteBtn.dataset.shareId = share.id;
deleteBtn.innerHTML = '<i class="fas fa-trash"></i> Delete';
actionsDiv.appendChild(deleteBtn);
shareEl.appendChild(actionsDiv);
existingSharesContainer.appendChild(shareEl);
});