| name | security |
| description | Security best practices for Electron and web applications. Use when implementing authentication, handling user input, managing secrets, or reviewing code for security. Triggers on security, auth, validation, sanitize, XSS, CSRF, injection, secrets, permissions. |
Security Best Practices
Security patterns and practices for building secure Electron applications with proper input validation, authentication, and defense against common vulnerabilities.
When to Use This Skill
- Implementing authentication/authorization
- Handling user input
- Managing secrets and API keys
- Processing external data
- Reviewing code for security issues
- Setting up Electron security
Core Security Principles
Defense in Depth
Layer multiple security controls:
function handleUserInput(input: string): void {
const validated = validateInput(input);
const sanitized = sanitizeForProcessing(validated);
await db.query('SELECT * FROM items WHERE name = ?', [sanitized]);
const encoded = encodeForHTML(sanitized);
}
Principle of Least Privilege
Request only necessary permissions:
mainWindow = new BrowserWindow({
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
}
});
mainWindow = new BrowserWindow({
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
webSecurity: true,
preload: path.join(__dirname, 'preload.js'),
}
});
Electron Security
Secure Window Configuration
export function createSecureWindow(): BrowserWindow {
const mainWindow = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
webSecurity: true,
allowRunningInsecureContent: false,
preload: path.join(__dirname, 'preload.js'),
},
});
mainWindow.webContents.on('will-navigate', (event, url) => {
const allowed = ['http://localhost:', 'file://'];
if (!allowed.some(prefix => url.startsWith(prefix))) {
event.preventDefault();
console.warn('Blocked navigation to:', url);
}
});
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: 'deny' };
});
return mainWindow;
}
Safe IPC Communication
contextBridge.exposeInMainWorld('api', {
exec: require('child_process').exec,
readFile: require('fs').readFileSync,
});
contextBridge.exposeInMainWorld('api', {
getSession: (id: string) => ipcRenderer.invoke('session:get', id),
startSession: (outcomeId: string) => ipcRenderer.invoke('session:start', outcomeId),
onSessionUpdate: (callback: (data: unknown) => void) => {
ipcRenderer.on('session:update', (_, data) => callback(data));
return () => ipcRenderer.removeListener('session:update', callback);
},
});
ipcMain.handle('session:get', async (event, id: unknown) => {
if (typeof id !== 'string') {
throw new Error('Invalid session ID');
}
if (!/^[a-zA-Z0-9-]{1,50}$/.test(id)) {
throw new Error('Invalid session ID format');
}
if (event.senderFrame.url !== expectedURL) {
throw new Error('Unauthorized sender');
}
return sessionService.getSession(id);
});
Content Security Policy
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': [
"default-src 'self'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"font-src 'self'",
"connect-src 'self' https://api.openai.com https://api.anthropic.com",
].join('; ')
}
});
});
Input Validation
Schema-Based Validation
import { z } from 'zod';
const OutcomeSchema = z.object({
id: z.string().uuid(),
title: z.string().min(1).max(500),
description: z.string().max(5000).optional(),
status: z.enum(['pending', 'active', 'completed']),
createdAt: z.coerce.date(),
});
const CreateTaskSchema = z.object({
outcomeId: z.string().uuid(),
title: z.string().min(1).max(200).trim(),
priority: z.number().int().min(1).max(5).default(3),
});
async function createTask(input: unknown): Promise<Task> {
const validated = CreateTaskSchema.parse(input);
return db.insert(tasks).values({
id: generateId(),
...validated,
}).returning().get();
}
Type Guards for Runtime Safety
function isValidAgentResponse(data: unknown): data is AgentResponse {
if (typeof data !== 'object' || data === null) return false;
const obj = data as Record<string, unknown>;
return (
typeof obj.id === 'string' &&
typeof obj.content === 'string' &&
(obj.status === 'complete' || obj.status === 'streaming')
);
}
async function processAgentResponse(raw: unknown) {
if (!isValidAgentResponse(raw)) {
throw new Error('Invalid agent response format');
}
return raw;
}
Injection Prevention
SQL Injection
const user = db.query(`SELECT * FROM users WHERE id = '${userId}'`);
const user = db.query('SELECT * FROM users WHERE id = ?', [userId]);
const user = await db.select()
.from(users)
.where(eq(users.id, userId))
.get();
const column = userInput;
db.query(`SELECT ${column} FROM users`);
const allowedColumns = ['id', 'name', 'email'] as const;
function selectColumn(column: string) {
if (!allowedColumns.includes(column as any)) {
throw new Error('Invalid column');
}
return db.select({ [column]: users[column] }).from(users);
}
Command Injection
import { exec } from 'child_process';
exec(`grep ${userInput} /var/log/app.log`);
import { execFile } from 'child_process';
execFile('grep', [userInput, '/var/log/app.log']);
import * as fs from 'fs/promises';
const content = await fs.readFile('/var/log/app.log', 'utf8');
const matches = content.split('\n').filter(line => line.includes(userInput));
Path Traversal
async function readUserFile(filename: string) {
return fs.readFile(`./uploads/${filename}`);
}
import * as path from 'path';
async function readUserFile(filename: string) {
const uploadsDir = path.resolve('./uploads');
const filePath = path.resolve(uploadsDir, filename);
if (!filePath.startsWith(uploadsDir + path.sep)) {
throw new Error('Invalid file path');
}
if (!/^[\w.-]+$/.test(path.basename(filePath))) {
throw new Error('Invalid filename');
}
return fs.readFile(filePath);
}
Secrets Management
Never Commit Secrets
.env
.env.local
.env.*.local
*.pem
*.key
secrets/
Environment Variables
const apiKey = 'sk-1234567890';
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
throw new Error('OPENAI_API_KEY not configured');
}
function loadConfig() {
const config = {
openaiKey: process.env.OPENAI_API_KEY,
anthropicKey: process.env.ANTHROPIC_API_KEY,
dbPath: process.env.DATABASE_PATH ?? './data/agentop.db',
};
if (!config.openaiKey && !config.anthropicKey) {
throw new Error('At least one AI provider key required');
}
return config;
}
Electron Secure Storage
import { safeStorage } from 'electron';
function storeApiKey(key: string): Buffer {
if (!safeStorage.isEncryptionAvailable()) {
throw new Error('Encryption not available');
}
return safeStorage.encryptString(key);
}
function retrieveApiKey(encrypted: Buffer): string {
return safeStorage.decryptString(encrypted);
}
const config = {
apiKey: storeApiKey(userProvidedKey).toString('base64'),
};
XSS Prevention
Render User Content Safely
function Message({ html }: { html: string }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
function Message({ text }: { text: string }) {
return <div>{text}</div>;
}
import DOMPurify from 'dompurify';
function SafeHTML({ html }: { html: string }) {
const clean = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'code', 'pre'],
ALLOWED_ATTR: ['class'],
});
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
import ReactMarkdown from 'react-markdown';
function MarkdownContent({ content }: { content: string }) {
return (
<ReactMarkdown
allowedElements={['p', 'code', 'pre', 'strong', 'em', 'ul', 'ol', 'li']}
>
{content}
</ReactMarkdown>
);
}
URL Validation
shell.openExternal(userProvidedUrl);
function openSafeURL(url: string): boolean {
try {
const parsed = new URL(url);
if (parsed.protocol !== 'https:') {
console.warn('Blocked non-HTTPS URL:', url);
return false;
}
const allowedDomains = ['github.com', 'docs.example.com'];
if (!allowedDomains.includes(parsed.hostname)) {
console.warn('Blocked untrusted domain:', parsed.hostname);
return false;
}
shell.openExternal(url);
return true;
} catch {
console.warn('Invalid URL:', url);
return false;
}
}
Security Checklist
Before Shipping
Code Review Security Questions
| Question | Why It Matters |
|---|
| Is user input validated at entry? | Prevents injection attacks |
| Are queries parameterized? | Prevents SQL injection |
| Is output encoded/sanitized? | Prevents XSS |
| Are file paths validated? | Prevents path traversal |
| Are secrets in environment variables? | Prevents credential leaks |
| Is contextIsolation enabled? | Prevents renderer privilege escalation |
| Are IPC handlers validating senders? | Prevents unauthorized access |