Enforce header-based CSP and block window open/navigation
This commit is contained in:
parent
4fb789de37
commit
4d4dfde1de
2 changed files with 70 additions and 5 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { app, BrowserWindow, clipboard, dialog, ipcMain, protocol } from 'electron';
|
||||
import { app, BrowserWindow, clipboard, dialog, ipcMain, protocol, shell } from 'electron';
|
||||
import { spawn, type ChildProcess } from 'node:child_process';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
|
@ -26,6 +26,42 @@ protocol.registerSchemesAsPrivileged([
|
|||
{ scheme: 'app', privileges: { standard: true, secure: true, supportFetchAPI: true } },
|
||||
]);
|
||||
|
||||
/**
|
||||
* Content-Security-Policy applied to every page this app loads.
|
||||
*
|
||||
* The production policy forbids inline scripts entirely (the Vite bundle is
|
||||
* external), so an injected `<script>` in rendered content cannot execute. The
|
||||
* dev-server policy keeps `'unsafe-inline'` because Vite's React-refresh
|
||||
* preamble is an inline script, but still pins network access to localhost
|
||||
* (HMR websocket included).
|
||||
*/
|
||||
const CSP_PROD =
|
||||
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; " +
|
||||
"connect-src 'self'; img-src 'self' data: https:; object-src 'none'; " +
|
||||
"base-uri 'none'; form-action 'none'";
|
||||
const CSP_DEV =
|
||||
"default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; " +
|
||||
"connect-src 'self' ws://localhost:* http://localhost:*; img-src 'self' data: https:; " +
|
||||
"object-src 'none'; base-uri 'none'; form-action 'none'";
|
||||
|
||||
/** The CSP for a URL this window may load, or `null` for anywhere else. */
|
||||
function cspForUrl(url: string): string | null {
|
||||
if (url.startsWith('app://')) {
|
||||
return CSP_PROD;
|
||||
}
|
||||
const devServer = process.env.NOSTR_GUI_DEV_URL;
|
||||
if (devServer) {
|
||||
try {
|
||||
if (new URL(url).origin === new URL(devServer).origin) {
|
||||
return CSP_DEV;
|
||||
}
|
||||
} catch {
|
||||
// Fall through: not a URL we recognise.
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
interface PendingRequest {
|
||||
resolve: (value: unknown) => void;
|
||||
reject: (reason: Error) => void;
|
||||
|
|
@ -390,6 +426,39 @@ function createWindow(): void {
|
|||
} else {
|
||||
void window.loadURL('app://nfm/index.html');
|
||||
}
|
||||
|
||||
// Stamp every top-level document with the matching CSP.
|
||||
window.webContents.session.webRequest.onHeadersReceived((details, callback) => {
|
||||
if (details.resourceType !== 'mainFrame') {
|
||||
callback({});
|
||||
return;
|
||||
}
|
||||
const csp = cspForUrl(details.url);
|
||||
if (!csp) {
|
||||
callback({});
|
||||
return;
|
||||
}
|
||||
callback({
|
||||
responseHeaders: { ...details.responseHeaders, 'Content-Security-Policy': [csp] },
|
||||
});
|
||||
});
|
||||
|
||||
// The app window never navigates away from its own origin; external links
|
||||
// open in the system browser instead. Deny everything unrecognised outright.
|
||||
window.webContents.on('will-navigate', (event, url) => {
|
||||
if (cspForUrl(url) === null) {
|
||||
event.preventDefault();
|
||||
if (/^https?:/i.test(url)) {
|
||||
void shell.openExternal(url).catch(() => {});
|
||||
}
|
||||
}
|
||||
});
|
||||
window.webContents.setWindowOpenHandler(({ url }) => {
|
||||
if (/^https?:/i.test(url)) {
|
||||
void shell.openExternal(url).catch(() => {});
|
||||
}
|
||||
return { action: 'deny' };
|
||||
});
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue