feat(security): HttpOnly cookies + CSP headers + CSRF double-submit protection

- Migrate auth tokens from localStorage to HttpOnly SameSite=Lax cookies
- Add cookie_auth.rs: helpers for setting/clearing auth + CSRF cookies
- Update auth middleware: 3-method auth (Bearer → Basic → Cookie)
- Add 5 security headers: CSP, X-Content-Type-Options, X-Frame-Options,
  Referrer-Policy, Permissions-Policy
- Implement CSRF double-submit cookie pattern (csrf.rs middleware)
- Set CSRF cookie on login/refresh/oidc-exchange, clear on logout
- CookieAuthenticated marker skips CSRF for Bearer/Basic clients
- Frontend: strip all localStorage token refs from 14 JS files
- Frontend: csrf.js utility + all 52 mutating fetch/XHR calls protected
- 121 tests passing, 0 warnings
This commit is contained in:
Dionisio
2026-03-03 01:10:50 +01:00
parent 7b2a8577a9
commit d2c08d31ba
27 changed files with 579 additions and 455 deletions
+3 -9
View File
@@ -127,9 +127,7 @@ const contextMenus = {
if (window.app.contextMenuTargetFile) {
// Capture reference before context menu cleanup nullifies it
const file = window.app.contextMenuTargetFile;
const token = localStorage.getItem('oxicloud_token');
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
fetch(`/api/files/${file.id}?metadata=true`, { headers })
fetch(`/api/files/${file.id}?metadata=true`, { credentials: 'same-origin' })
.then(response => response.json())
.then(fileDetails => {
// Check if viewable file type (images, PDFs, text files)
@@ -585,8 +583,6 @@ const contextMenus = {
*/
async loadMoveDialogFolders(parentFolderId) {
try {
const token = localStorage.getItem('oxicloud_token');
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
// Ensure we have the home folder ID before proceeding
if (!window.app.userHomeFolderId) {
@@ -606,7 +602,7 @@ const contextMenus = {
const url = `/api/folders/${effectiveParentId}/contents`;
console.log('[Move Dialog] Loading folders from:', url, 'effectiveParentId:', effectiveParentId);
const response = await fetch(url, { headers });
const response = await fetch(url, { credentials: 'same-origin' });
if (!response.ok) {
console.error('Failed to load folders:', response.status);
return;
@@ -1023,9 +1019,7 @@ const contextMenus = {
};
try {
const token = localStorage.getItem('oxicloud_token');
const headers = { 'Content-Type': 'application/json' };
if (token) headers['Authorization'] = `Bearer ${token}`;
const headers = { 'Content-Type': 'application/json', ...getCsrfHeaders() };
const response = await fetch('/api/shares', {
method: 'POST',
+8 -11
View File
@@ -4,16 +4,12 @@
*/
/**
* Get authorization headers for API requests
* @returns {Object} Headers object with Authorization bearer token
* Get authorization headers for API requests.
* Tokens are now in HttpOnly cookies — no explicit Authorization header needed.
* @returns {Object} Headers object
*/
function getAuthHeaders() {
const token = localStorage.getItem('oxicloud_token');
const headers = {};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
return headers;
return { ...getCsrfHeaders() };
}
// File Operations Module
@@ -177,10 +173,11 @@ const fileOps = {
xhr.open('POST', '/api/files/upload');
// Set auth header
const token = localStorage.getItem('oxicloud_token');
if (token) xhr.setRequestHeader('Authorization', `Bearer ${token}`);
// Auth is handled by HttpOnly cookies — no explicit header needed
xhr.setRequestHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
// CSRF double-submit: echo the CSRF cookie as a request header
const _csrfTok = getCsrfToken();
if (_csrfTok) xhr.setRequestHeader('X-CSRF-Token', _csrfTok);
try {
xhr.send(formData);
+5 -18
View File
@@ -210,10 +210,7 @@ class InlineViewer {
try {
console.log('Creating text viewer for:', file.name);
const token = localStorage.getItem('oxicloud_token');
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
const response = await fetch(`/api/files/${file.id}?inline=true`, { headers });
const response = await fetch(`/api/files/${file.id}?inline=true`, { credentials: 'same-origin' });
if (!response.ok) {
throw new Error(`Error fetching file: ${response.status} ${response.statusText}`);
@@ -254,12 +251,7 @@ class InlineViewer {
const xhr = new XMLHttpRequest();
xhr.open('GET', `/api/files/${file.id}?inline=true`, true);
xhr.responseType = 'blob';
// Add auth header
const token = localStorage.getItem('oxicloud_token');
if (token) {
xhr.setRequestHeader('Authorization', `Bearer ${token}`);
}
xhr.withCredentials = true;
// Create a promise to handle the XHR
const response = await new Promise((resolve, reject) => {
@@ -357,10 +349,8 @@ class InlineViewer {
try {
console.log(`Creating ${mediaType} player for:`, file.name);
// Fetch file with auth header (same pattern as images/PDFs)
const token = localStorage.getItem('oxicloud_token');
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
const response = await fetch(`/api/files/${file.id}?inline=true`, { headers });
// Fetch file (cookie auto-sent)
const response = await fetch(`/api/files/${file.id}?inline=true`, { credentials: 'same-origin' });
if (!response.ok) {
throw new Error(`Error fetching file: ${response.status} ${response.statusText}`);
@@ -488,10 +478,7 @@ class InlineViewer {
}
downloadFile(file) {
const token = localStorage.getItem('oxicloud_token');
const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
fetch(`/api/files/${file.id}`, { headers })
fetch(`/api/files/${file.id}`, { credentials: 'same-origin' })
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.blob();
+1 -4
View File
@@ -60,12 +60,9 @@ class WopiEditor {
* Fetch editor URL and WOPI token from the backend.
*/
async _getEditorUrl(fileId, action) {
var token = localStorage.getItem('oxicloud_token') || '';
var response = await fetch(
'/api/wopi/editor-url?file_id=' + encodeURIComponent(fileId) + '&action=' + encodeURIComponent(action),
{
headers: { 'Authorization': 'Bearer ' + token }
}
{ credentials: 'same-origin' }
);
if (!response.ok) {
var text = await response.text();