Reputation: 75
I'm making a desktop application with Electron and Node.js, in which I'm trying to implement CSP /nonce.
So far I have developed a script that should generate the content security policy:
const crypto = require('crypto');
class SecurityPolicies {
static NONCE_LENGTH = 32;
static CSP_DIRECTIVES = {
'default-src': ["'self'"],
'script-src': ["'self'", "'unsafe-inline'"],
'style-src': ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
'img-src': ["'self'", "data:", "https:", "blob:"],
'font-src': ["'self'", "data:","https://fonts.gstatic.com"],
'object-src': ["'none'"],
'base-uri': ["'self'"],
'form-action': ["'self'"],
'frame-ancestors': ["'none'"],
'connect-src': ["'self'","ws:", "wss:"],
'media-src': ["'self'", "blob:"],
'worker-src': ["'self'", "blob:"],
'manifest-src': ["'self'"]
};
constructor() {
this.nonce = this.generateNonce();
this.contentSecurityPolicy = this.buildCSP();
}
generateNonce() {
return crypto.randomBytes(SecurityPolicies.NONCE_LENGTH).toString('base64');
}
buildCSP() {
const directives = {...SecurityPolicies.CSP_DIRECTIVES};
directives['script-src'] = [
...directives['script-src'],
`'unsafe-inline'`,
`'nonce-${this.nonce}'`
];
directives['style-src'] = [
...directives['style-src'],
`'unsafe-inline'`,
`'nonce-${this.nonce}'`
];
return Object.entries(directives)
.map(([key, values]) => `${key} ${values.join(' ')}`)
.join('; ');
}
applySecurityPolicy(window) {
window.webContents.session.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': [this.contentSecurityPolicy],
'X-Content-Type-Options': ['nosniff'],
'X-Frame-Options': ['DENY'],
'X-XSS-Protection': ['1; mode=block'],
'Referrer-Policy': ['strict-origin-when-cross-origin'],
'Permissions-Policy': ['camera=(), microphone=(), geolocation=()']
}
});
});
// Disable navigation
window.webContents.on('will-navigate', (event, url) => {
const parsedUrl = new URL(url);
if (parsedUrl.origin !== 'file://') {
event.preventDefault();
}
});
// Block new window creation
window.webContents.setWindowOpenHandler(() => ({ action: 'deny' }));
// Disable all permission requests
window.webContents.session.setPermissionRequestHandler((_, __, callback) => {
callback(false);
});
// Disable remote content
window.webContents.session.setPreloads([]);
}
static initSecurity(window) {
const securityPolicies = new SecurityPolicies();
securityPolicies.applySecurityPolicy(window);
return securityPolicies;
}
getNonce() {
return this.nonce;
}
getCSP() {
return this.contentSecurityPolicy;
}
}
module.exports = SecurityPolicies;
The main idea is to apply nonce to the elements which are then incorporated my index.html
looks like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'nonce-<%nonce%>'; style-src 'self' 'nonce-<%nonce%>';">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebStack Deployer for Docker</title>
<link rel="stylesheet" href="assets/css/bootstrap.css" nonce="<%nonce%>">
<link rel="stylesheet" href="assets/css/styles.css" nonce="<%nonce%>">
</head>
<body class="d-flex vh-100 vw-100">
<div class="title-bar top">
<div class="dropdown">
<button class="btn btn-dark dropbtn" id="maintenanceBtn" data-bs-toggle="dropdown" aria-expanded="false">
Maintenance
</button>
<div class="dropdown-content dropdown-menu" aria-labelledby="maintenanceBtn">
<a class="dropdown-item" href="#" id="option1">Check for Updates</a>
<a class="dropdown-item" href="#" id="option2">Backup Settings</a>
<a class="dropdown-item" href="#" id="option3">Clear Cache</a>
<a class="dropdown-item" href="#" id="option4">Reset Preferences</a>
<div class="dropdown-divider"></div>
<a class="dropdown-item" href="#" id="option5">Exit</a>
</div>
</div>
<div class="top-right-controls">
<button class="btn btn-dark control-button" id="settingsBtn">Settings</button>
<button class="btn btn-dark control-button" id="helpBtn">Help</button>
<button class="btn btn-dark control-button" id="aboutBtn">About</button>
<button class="btn window-control minimize" id="minimizeBtn">─</button>
<button class="btn window-control maximize" id="maximizeBtn">□</button>
<button class="btn window-control close" id="closeBtn">×</button>
</div>
</div>
<div class="content container-fluid">
<h1 class="display-4">WebStack Deployer for Docker</h1>
</div>
<script src="assets/js/bootstrap.bundle.min.js" nonce="<%nonce%>"></script>
<script src="assets/js/renderer.js" nonce="<%nonce%>"></script>
</body>
</html>
As you can see, I have implemented this token <%nonce%>
which should be replaced by the generated nonce string nonce.
I assumed I could use a content loader:
async loadContent() {
const isDev = process.env.NODE_ENV === 'development';
const nonce = this.securityPolicies.getNonce();
if (isDev) {
await this.mainWindow.loadURL('http://localhost:3000');
} else {
const htmlPath = path.join(__dirname, '../index.html');
let htmlContent = readFileSync(htmlPath, 'utf8');
htmlContent = htmlContent.replace(/<%nonce%>/g, nonce);
await this.mainWindow.loadFile(htmlPath, {
query: {nonce: nonce}
});
}
}
Here is where the problem is, if I already saw that I generate htmlContent
and I don't use it... this is because in order to use it I must generate a uri but I always get an error about the malformed uri, what other way can I implement it?
windows manager class:
const { BrowserWindow } = require('electron');
const path = require('node:path');
const SecurityPolicies = require('../security/SecurityPolicies');
const {readFileSync} = require("node:fs");
class WindowManager {
constructor() {
this.mainWindow = null;
this.securityPolicies = null;
}
async createMainWindow() {
this.mainWindow = new BrowserWindow({
width: 1200,
height: 800,
frame: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
webSecurity: true,
preload: path.join(__dirname, '../preload.js'),
sandbox: true
}
});
this.securityPolicies = SecurityPolicies.initSecurity(this.mainWindow);
this.mainWindow.maximize();
await this.loadContent();
return new Promise((resolve) => {
this.mainWindow.once('ready-to-show', () => {
resolve(this.mainWindow);
});
this.mainWindow.on('closed', () => {
this.mainWindow = null;
this.securityPolicies = null;
});
});
}
async loadContent() {
const isDev = process.env.NODE_ENV === 'development';
const nonce = this.securityPolicies.getNonce();
if (isDev) {
await this.mainWindow.loadURL('http://localhost:3000');
} else {
const htmlPath = path.join(__dirname, '../index.html');
let htmlContent = readFileSync(htmlPath, 'utf8');
htmlContent = htmlContent.replace(/<%nonce%>/g, nonce);
await this.mainWindow.loadFile(htmlPath, {
query: {nonce: nonce}
});
}
}
getMainWindow() {
return this.mainWindow;
}
getSecurityPolicies() {
return this.securityPolicies;
}
}
module.exports = WindowManager;
main.js class:
const {app} = require('electron');
const AppConfig = require('./models/AppConfig');
const BackendController = require('./controllers/BackendController');
const WindowManager = require('./views/WindowManager');
const IpcController = require('./controllers/IpcController');
const ApplicationSetup = require('./core/ApplicationSetup');
const DevToolsManager = require('./core/DevToolsManager');
const AppLifecycle = require('./core/AppLifecycle');
class Application {
constructor() {
this.appConfig = new AppConfig();
this.backendController = new BackendController();
this.windowManager = null;
this.mainWindow = null;
this.ipcController = null;
}
async initialize() {
ApplicationSetup.configureCommandLineFlags();
app.setPath('userData', this.appConfig.getUserDataPath());
// Enable sandbox for all renderers
app.enableSandbox();
// Disable navigation outside the app
app.on('web-contents-created', (_, contents) => {
contents.on('will-navigate', (event) => {
event.preventDefault();
});
contents.setWindowOpenHandler(() => ({ action: 'deny' }));
});
try {
await this.backendController.initializeFetch();
await this.setupAppEvents();
return this.ipcController;
} catch (error) {
console.error('Initialization error:', error);
throw error;
}
}
async setupAppEvents() {
await app.whenReady();
try {
await this.backendController.startGoBackend();
this.windowManager = new WindowManager();
this.mainWindow = await this.windowManager.createMainWindow();
if (this.mainWindow) {
this.ipcController = new IpcController(
this.windowManager,
this.backendController,
this.appConfig
);
// Only enable DevTools in development
//if (process.env.NODE_ENV === 'development') {
DevToolsManager.setupDevToolsHandlers(this.mainWindow);
//}
}
AppLifecycle.setupAppEventHandlers(this.windowManager, this.backendController);
ApplicationSetup.setupProcessEventHandlers(this.backendController);
} catch (error) {
console.error('Setup error:', error);
throw error;
}
}
}
const application = new Application();
application.initialize().catch(err => {
console.error('Application initialization failed:', err);
app.quit();
});
module.exports = application;
Update
I got it working but I'm not sure I'm doing it correctly; I'm just removing the class SecurityPolicies
and can be used directly in this way:
this.mainWindow = new BrowserWindow({
width: 1200,
height: 800,
frame: false,
icon: path.join(__dirname, '../assets/icons', process.platform === 'win32' ? 'icon.ico' : 'icon.icns'),
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
webSecurity: true,
preload: path.join(__dirname, '../preload.js'),
//sandbox: true,
experimentalFeatures: false
}
});
const cspDirectives = [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval'",
"style-src 'self' 'unsafe-inline' 'unsafe-eval' https: data:",
"img-src 'self' data: https:",
"font-src 'self' data: https:",
"connect-src 'self' http://localhost:8080 ws://localhost:3000",
"base-uri 'self'",
"form-action 'self'",
"worker-src 'self' blob:",
"media-src 'self' blob: data:",
"object-src 'none'"
].join('; ');
this.mainWindow.webContents.session.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': [cspDirectives]
}
});
});
And on index.html
i add this to the head
:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data:;">
This without implementing /nonce because of problem in deploy contents
Upvotes: 3
Views: 101