This document describes the security architecture of the Ledger application, including Electron sandbox configuration, IPC security, command injection prevention, and plugin permission system.
Ledger uses Electron's security best practices with explicit configuration in lib/main/app.ts:
webPreferences: {
preload: join(__dirname, '../preload/preload.js'),
sandbox: true, // V8 sandbox enabled
contextIsolation: true, // Preload isolated from renderer
nodeIntegration: false, // No Node.js in renderer
nodeIntegrationInWorker: false, // No Node.js in workers
webSecurity: true, // Same-origin policy enforced
}Why these settings matter:
sandbox: true: Enables Chromium's OS-level sandboxing, restricting what the renderer process can accesscontextIsolation: true: Runs preload scripts in an isolated context, preventing renderer code from accessing Node.js APIs or modifying preload globalsnodeIntegration: false: Prevents renderer code from accessing Node.js APIs directlywebSecurity: true: Enforces same-origin policy, preventing cross-origin requests
The preload script (lib/preload/preload.ts) includes a runtime check to catch configuration errors:
if (!process.contextIsolated) {
console.error('[SECURITY] Context isolation is DISABLED! This is a security risk.')
}The application uses a restrictive CSP in app/index.html:
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self';
style-src 'self' 'unsafe-inline'; img-src 'self' data: res:;"
/>This prevents:
- Loading scripts from external sources
- Inline script execution (except trusted preload)
- Loading resources from unauthorized origins
All renderer-to-main process communication uses ipcRenderer.invoke() with typed channels:
// Preload exposes limited API via contextBridge
contextBridge.exposeInMainWorld('conveyor', {
repo: new RepoApi(),
branch: new BranchApi(),
// ... other APIs
})
// Main process validates all inputs with Zod schemas
handle('channel-name', async (data) => {
const validated = schema.parse(data)
// ... process validated data
})IPC handlers use safe error serialization (lib/utils/error-helpers.ts) to prevent leaking sensitive information:
export function serializeError(error: unknown): string {
if (error instanceof Error) return error.message
if (typeof error === 'string') return error
if (typeof error === 'object' && error !== null && 'message' in error) {
return String((error as { message: unknown }).message)
}
return 'Unknown error'
}The application uses safeExec() (lib/utils/safe-exec.ts) for all shell commands:
export const safeExec = async (
command: string,
args: string[],
options?: { cwd?: string; timeout?: number }
): Promise<ExecResult> => {
return new Promise((resolve) => {
const proc = spawn(command, args, {
cwd: options?.cwd,
timeout: options?.timeout ?? 30000,
shell: false, // CRITICAL: No shell interpolation
})
// ...
})
}Key security feature: shell: false ensures arguments are passed directly to the process without shell interpretation, preventing injection attacks.
export function isValidNpmPackageName(name: string): boolean {
return /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(name)
}Prevents: lodash; rm -rf /, $(whoami), backtick injection
function isValidGitUrl(url: string): boolean {
return /^(https:\/\/|git@)[a-zA-Z0-9.-]+[/:][a-zA-Z0-9._/-]+\.git$/.test(url)
}Prevents: https://evil.com/; rm -rf /, command substitution
File path operations validate against directory traversal attacks:
// Security: Validate path doesn't contain traversal attempts
const resolvedPath = path.resolve(folderPath)
if (folderPath.includes('..') || resolvedPath !== path.normalize(folderPath)) {
return { success: false, message: 'Invalid folder path: path traversal not allowed' }
}
// Security: Ensure path is absolute to prevent relative path attacks
if (!path.isAbsolute(folderPath)) {
return { success: false, message: 'Folder path must be absolute' }
}| Operation | Protection Method |
|---|---|
| Plugin git clone | simple-git library (no shell) |
| NPM pack | safeExec + validation |
| PR create/comment | safeExec (args array) |
| PR merge | safeExec (args array) |
| Open URL | safeExec (URL as single arg) |
| Worktree creation | Path traversal validation |
type PluginPermission =
| 'git:read' // Read repository info
| 'git:write' // Perform git operations
| 'fs:read' // Read files
| 'fs:write' // Write files
| 'network' // Make network requests
| 'shell' // Execute shell commands
| 'clipboard' // Access clipboard
| 'notifications' // Show notifications- Plugin declares required permissions in manifest
- During activation,
pluginLoader.requestPermissions()is called - UI displays
PermissionDialogwith risk levels - User approves/denies specific permissions
- Approved permissions stored in
pluginRegistry PluginAPImethods check permissions before execution
Plugin API methods in lib/plugins/plugin-context.ts check permissions:
const checkPermission = (permission: PluginPermission): boolean => {
if (!hasPermission(pluginId, permission)) {
logger.warn(`Missing permission: ${permission}`)
return false
}
return true
}
// Example: git:read required
getBranches: async () => {
if (!checkPermission('git:read')) return []
return deps.getBranches()
}- Built-in plugins: Auto-approved (trusted source)
- Local plugins: Require user approval
- External plugins (git/npm): Require user approval + high-risk warning
-
Never use string interpolation for shell commands
// BAD exec(`gh pr comment ${prNumber} --body "${body}"`) // GOOD safeExec('gh', ['pr', 'comment', prNumber.toString(), '--body', body])
-
Always validate external input
if (!isValidNpmPackageName(name)) { return { success: false, message: 'Invalid package name' } }
-
Use Zod schemas for IPC validation
const schema = z.object({ path: z.string().min(1), branch: z.string().regex(/^[a-zA-Z0-9._/-]+$/) })
-
Check permissions in plugin API methods
if (!hasPermission(pluginId, 'git:write')) { throw new Error('Permission denied: git:write required') }
Run the validation test suite:
npm test -- tests/validation.spec.tsTests cover:
- NPM package name validation
- Git URL validation
- Safe execution patterns
- Error serialization
If you discover a security vulnerability, please report it privately to the maintainers rather than opening a public issue.