Enhance security and offline functionality

- Implement stricter security measures in the Electron app, including navigation blocking, URL validation, and external request handling.
- Add offline mode handling and UI improvements in components like `ScribeFooterBar` and `AddNewBookForm`.
- Refactor `DeleteBook` logic to include offline sync methods.
- Improve user feedback for online/offline states and synchronization errors.
This commit is contained in:
natreex
2025-12-24 15:20:26 -05:00
parent 4bc6a40b38
commit a315e96633
5 changed files with 143 additions and 15 deletions

View File

@@ -64,6 +64,13 @@ function createLoginWindow(): void {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
webSecurity: true,
allowRunningInsecureContent: false,
experimentalFeatures: false,
enableBlinkFeatures: '',
disableBlinkFeatures: '',
webviewTag: false,
navigateOnDragDrop: false,
},
frame: true,
show: false,
@@ -84,6 +91,25 @@ function createLoginWindow(): void {
loginWindow.on('closed', () => {
loginWindow = null;
});
// Security: Block navigation to external domains
loginWindow.webContents.on('will-navigate', (event, navigationUrl) => {
const parsedUrl = new URL(navigationUrl);
if (isDev) {
if (!parsedUrl.origin.startsWith('http://localhost')) {
event.preventDefault();
}
} else {
if (parsedUrl.protocol !== 'scribedesktop:') {
event.preventDefault();
}
}
});
// Security: Block new window creation
loginWindow.webContents.setWindowOpenHandler(() => {
return { action: 'deny' };
});
}
function createMainWindow(): void {
@@ -97,6 +123,13 @@ function createMainWindow(): void {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
webSecurity: true,
allowRunningInsecureContent: false,
experimentalFeatures: false,
enableBlinkFeatures: '',
disableBlinkFeatures: '',
webviewTag: false,
navigateOnDragDrop: false,
},
show: false,
});
@@ -116,11 +149,43 @@ function createMainWindow(): void {
mainWindow.on('closed', () => {
mainWindow = null;
});
// Security: Block navigation to external domains
mainWindow.webContents.on('will-navigate', (event, navigationUrl) => {
const parsedUrl = new URL(navigationUrl);
if (isDev) {
if (!parsedUrl.origin.startsWith('http://localhost')) {
event.preventDefault();
}
} else {
if (parsedUrl.protocol !== 'scribedesktop:') {
event.preventDefault();
}
}
});
// Security: Block new window creation
mainWindow.webContents.setWindowOpenHandler(() => {
return { action: 'deny' };
});
}
// IPC Handler pour ouvrir des liens externes (navigateur/app native)
ipcMain.handle('open-external', async (_event, url: string) => {
await shell.openExternal(url);
// Security: Validate URL before opening
try {
const parsedUrl = new URL(url);
const allowedProtocols = ['http:', 'https:', 'mailto:'];
if (!allowedProtocols.includes(parsedUrl.protocol)) {
console.error('[Security] Blocked external URL with invalid protocol:', parsedUrl.protocol);
return;
}
await shell.openExternal(url);
} catch (error) {
console.error('[Security] Invalid URL rejected:', url);
}
});
// IPC Handlers pour la gestion du token (OS-encrypted storage)
@@ -347,8 +412,39 @@ ipcMain.handle('db-initialize', (_event, userId: string, encryptionKey: string)
});
app.whenReady().then(():void => {
// Menu minimal pour garder les raccourcis DevTools
// Security: Disable web cache in production
if (!isDev) {
app.commandLine.appendSwitch('disable-http-cache');
}
// Security: Set permissions request handler
app.on('web-contents-created', (_event, contents) => {
// Allow only clipboard permissions, block others
contents.session.setPermissionRequestHandler((_webContents, permission, callback) => {
const allowedPermissions: string[] = ['clipboard-read', 'clipboard-sanitized-write'];
callback(allowedPermissions.includes(permission));
});
// Block all web requests to file:// protocol
contents.session.protocol.interceptFileProtocol('file', (request, callback) => {
callback({ error: -3 }); // net::ERR_ABORTED
});
});
// Menu minimal pour garder les raccourcis DevTools et clipboard
const template: Electron.MenuItemConstructorOptions[] = [
{
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ role: 'selectAll' }
]
},
{
label: 'View',
submenu: [
@@ -364,10 +460,20 @@ app.whenReady().then(():void => {
const outPath:string = path.join(process.resourcesPath, 'app.asar.unpacked/out');
protocol.handle('scribedesktop', async (request) => {
// Security: Validate and sanitize file path
let filePath:string = request.url.replace('scribedesktop://', '').replace(/^\.\//, '');
// Security: Block path traversal attempts
if (filePath.includes('..') || filePath.includes('~')) {
console.error('[Security] Path traversal attempt blocked:', filePath);
return new Response('Forbidden', { status: 403 });
}
const fullPath:string = path.normalize(path.join(outPath, filePath));
// Security: Ensure path is within allowed directory
if (!fullPath.startsWith(outPath)) {
console.error('[Security] Path escape attempt blocked:', fullPath);
return new Response('Forbidden', { status: 403 });
}
@@ -389,7 +495,10 @@ app.whenReady().then(():void => {
};
return new Response(data, {
headers: { 'Content-Type': mimeTypes[ext] || 'application/octet-stream' }
headers: {
'Content-Type': mimeTypes[ext] || 'application/octet-stream',
'X-Content-Type-Options': 'nosniff'
}
});
} catch (error) {
return new Response('Not found', { status: 404 });