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 { spawn, type ChildProcess } from 'node:child_process';
|
||||||
import { randomBytes } from 'node:crypto';
|
import { randomBytes } from 'node:crypto';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
|
|
@ -26,6 +26,42 @@ protocol.registerSchemesAsPrivileged([
|
||||||
{ scheme: 'app', privileges: { standard: true, secure: true, supportFetchAPI: true } },
|
{ 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 {
|
interface PendingRequest {
|
||||||
resolve: (value: unknown) => void;
|
resolve: (value: unknown) => void;
|
||||||
reject: (reason: Error) => void;
|
reject: (reason: Error) => void;
|
||||||
|
|
@ -390,6 +426,39 @@ function createWindow(): void {
|
||||||
} else {
|
} else {
|
||||||
void window.loadURL('app://nfm/index.html');
|
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(() => {
|
app.whenReady().then(() => {
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,6 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta
|
|
||||||
http-equiv="Content-Security-Policy"
|
|
||||||
content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws: http://localhost:*; img-src 'self' data: https:; object-src 'none'; base-uri 'none'; form-action 'none'"
|
|
||||||
/>
|
|
||||||
<title>Nostr Feed Manager</title>
|
<title>Nostr Feed Manager</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue