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:
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user