Skip to content

Security Hardening#413

Open
pedramamini wants to merge 6 commits intomainfrom
hardening
Open

Security Hardening#413
pedramamini wants to merge 6 commits intomainfrom
hardening

Conversation

@pedramamini
Copy link
Collaborator

@pedramamini pedramamini commented Feb 18, 2026

Summary by CodeRabbit

  • New Features

    • Content Security Policy now enabled for enhanced protection
    • Improved splash screen with progress animation and error messaging
  • Bug Fixes

    • Enhanced HTML sanitization in markdown rendering
    • Stricter validation for external URLs (http:, https:, mailto: only)
    • Path traversal protection for local content
    • Improved window sandbox security and crash handling with auto-reload
    • Permission request denial enforcement
  • Tests

    • Added comprehensive security validation tests

…b to block stored XSS

Both components render agent-generated markdown that does not require raw
HTML passthrough. Removing allowRawHtml ensures rehype-raw is not enabled,
preventing any injected HTML/script tags in history files from executing
in the Electron renderer.
…path as defense-in-depth

When allowRawHtml is enabled, content is now pre-sanitized with
DOMPurify.sanitize() before passing to ReactMarkdown. This strips
script tags, event handlers, iframe injections, and javascript: URLs
even when raw HTML passthrough is active. Uses the same DOMPurify
pattern already established in TerminalOutput.tsx.
…pts to external files

Adds a strict CSP meta tag to index.html with script-src 'self' to block
inline script injection. Moves splash screen and React DevTools scripts
to external files in src/renderer/public/ for CSP compliance. Adds a
Vite transformIndexHtml hook to relax the CSP in dev mode so HMR and
React Refresh inline scripts continue to work.
…permission denial

- Add sandbox: true to webPreferences for OS-level renderer process sandboxing
- Add setWindowOpenHandler to deny all popup/new-window requests
- Add will-navigate handler to block navigation away from the app
- Add setPermissionRequestHandler to deny all browser permissions (camera, mic, etc.)
- Add 6 new tests covering all security hardening behaviors
@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

This PR implements comprehensive security hardening across the main process, IPC handlers, and renderer components. Changes include enabling renderer sandboxing, blocking popups and permissions, restricting navigation by protocol, implementing path traversal protections in marketplace handlers, adding HTML sanitization via DOMPurify, enforcing Content-Security-Policy headers, and adding extensive test coverage for security features.

Changes

Cohort / File(s) Summary
Main Process Security Hardening
src/main/app-lifecycle/window-manager.ts
Enables sandbox mode, denies all popup requests, restricts navigation to dev/file/app URLs, blocks all permission requests, and adds crash/reload handling with Sentry integration.
IPC Handler Path & Protocol Validation
src/main/ipc/handlers/marketplace.ts, src/main/ipc/handlers/system.ts
Adds path traversal protection via validateSafePath helper for local document/asset/readme access. Implements protocol whitelisting (http, https, mailto) for shell:openExternal with explicit rejection of file, javascript, and data URLs.
Renderer HTML Sanitization
src/renderer/components/MarkdownRenderer.tsx, src/renderer/components/.../AIOverviewTab.tsx, src/renderer/components/HistoryDetailModal.tsx
Integrates DOMPurify for sanitizing raw HTML in Markdown content. Removes allowRawHtml prop usage across components; exports CodeBlockWithCopy component.
Build & Dev Configuration
eslint.config.mjs, src/renderer/index.html, src/renderer/public/splash.js, src/renderer/public/devtools-connect.js, vite.config.mts
Adds ESLint ignores for src/renderer/public/**. Extracts inline splash screen and React DevTools scripts to external files for CSP compliance. Adds CSP meta tag to HTML head. Implements dev-mode CSP relaxation plugin for HMR/React Refresh.
Security Feature Test Coverage
src/__tests__/main/app-lifecycle/window-manager.test.ts, src/__tests__/main/ipc/handlers/marketplace.test.ts, src/__tests__/main/ipc/handlers/system.test.ts, src/__tests__/renderer/components/MarkdownRenderer.test.tsx
Comprehensive test suites validating window lifecycle restrictions, path traversal rejection, protocol validation, and HTML sanitization behavior across production and development modes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main objective of the pull request, which involves multiple security hardening measures across the codebase including sandboxing, path traversal protection, URL validation, and XSS sanitization.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch hardening

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link

greptile-apps bot commented Feb 18, 2026

Greptile Summary

Comprehensive security hardening across multiple attack surfaces in the Electron application. The changes implement defense-in-depth protections including Content Security Policy, Electron sandbox mode, protocol whitelisting, path traversal guards, and XSS sanitization.

Key Changes:

  • BrowserWindow Security: Enabled sandbox mode, blocked all popups via setWindowOpenHandler, restricted navigation to app-only URLs, and denied all permission requests (camera, mic, geolocation)
  • Protocol Whitelist: Restricted shell:openExternal to http/https/mailto only, blocking dangerous protocols like file://, javascript:, and data:
  • Path Traversal Protection: Added validateSafePath() function and .. checks to prevent directory traversal in marketplace file reads
  • XSS Prevention: Added DOMPurify sanitization for markdown with allowRawHtml, removed allowRawHtml from AI-generated and history content
  • CSP Implementation: Added strict Content Security Policy to index.html, extracted inline scripts to external files, relaxed CSP in dev mode for Vite HMR
  • Test Coverage: Comprehensive tests added for all security features

One Logic Issue Found:

  • validateSafePath in marketplace.ts:350-356 has an edge case where prefix matching could allow traversal (e.g., base /Users/foo would incorrectly allow /Users/foobar/). Should use path.relative() instead.

Confidence Score: 4/5

  • This PR significantly improves security posture but has one path traversal logic bug that needs fixing before merge
  • Score reflects excellent security hardening work with comprehensive test coverage across Electron security best practices (CSP, sandbox, protocol restrictions, XSS prevention). However, the validateSafePath implementation has a prefix-matching bug that could allow directory traversal in specific edge cases. Once this logic issue is fixed, this would be a 5/5.
  • src/main/ipc/handlers/marketplace.ts requires fixing the validateSafePath logic before merge

Important Files Changed

Filename Overview
src/main/app-lifecycle/window-manager.ts Added sandbox mode, window open blocking, navigation restrictions, and permission denial for BrowserWindow security hardening
src/main/ipc/handlers/system.ts Added protocol whitelist (http/https/mailto) to shell:openExternal, blocking dangerous protocols like file://, javascript:, data:
src/main/ipc/handlers/marketplace.ts Added path traversal guards with validateSafePath() and ".." checks to prevent directory traversal attacks in file reads
src/renderer/components/MarkdownRenderer.tsx Added DOMPurify sanitization for raw HTML to prevent XSS attacks when allowRawHtml is enabled
src/renderer/index.html Added Content Security Policy meta tag and extracted inline scripts to external files for CSP compliance

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Renderer Process] -->|External URL| B{shell:openExternal}
    B -->|Validate Protocol| C{Protocol Check}
    C -->|http/https/mailto| D[Allow - Open in Browser]
    C -->|file:/javascript:/data:| E[Reject - Throw Error]
    
    A -->|window.open| F{setWindowOpenHandler}
    F -->|Any URL| G[Deny All Popups]
    
    A -->|Navigation Attempt| H{will-navigate Handler}
    H -->|Production| I{Protocol Check}
    H -->|Development| J{Origin Check}
    I -->|file://| K[Allow App Navigation]
    I -->|Other| L[Block - preventDefault]
    J -->|Dev Server Origin| K
    J -->|Other Origin| L
    
    A -->|Permission Request| M{setPermissionRequestHandler}
    M -->|camera/mic/geo/etc| N[Deny All Permissions]
    
    A -->|Local File Read| O{marketplace:getDocument}
    O -->|Validate Filename| P{Contains .. ?}
    P -->|Yes| Q[Reject - Invalid Filename]
    P -->|No| R{validateSafePath}
    R -->|Outside Base| S[Reject - Path Traversal]
    R -->|Inside Base| T[Allow - Read File]
    
    A -->|Render Markdown| U{allowRawHtml?}
    U -->|true| V[DOMPurify.sanitize]
    U -->|false| W[Standard Markdown]
    V -->|Strip XSS| X[Safe HTML Output]
    W -->|No HTML| X
    
    Y[index.html] -->|CSP Meta Tag| Z[Content Security Policy]
    Z -->|Production| AA[Strict: script-src 'self']
    Z -->|Development| AB[Relaxed: unsafe-inline for HMR]
Loading

Last reviewed commit: 7ee177a

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

15 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +350 to +356
function validateSafePath(basePath: string, requestedFile: string): string {
const realBase = path.resolve(basePath);
const resolved = path.resolve(basePath, requestedFile);
if (!resolved.startsWith(realBase + path.sep) && resolved !== realBase) {
throw new MarketplaceFetchError(`Path traversal blocked: ${requestedFile}`);
}
return resolved;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

edge case: validateSafePath could allow traversal if basePath="/Users/foo" and someone creates /Users/foobar/ directory, since "/Users/foobar/file" would start with "/Users/foo". should use path.relative() and check it doesn't start with .. instead

Suggested change
function validateSafePath(basePath: string, requestedFile: string): string {
const realBase = path.resolve(basePath);
const resolved = path.resolve(basePath, requestedFile);
if (!resolved.startsWith(realBase + path.sep) && resolved !== realBase) {
throw new MarketplaceFetchError(`Path traversal blocked: ${requestedFile}`);
}
return resolved;
function validateSafePath(basePath: string, requestedFile: string): string {
const realBase = path.resolve(basePath);
const resolved = path.resolve(basePath, requestedFile);
const relative = path.relative(realBase, resolved);
if (relative.startsWith('..') || path.isAbsolute(relative)) {
throw new MarketplaceFetchError(`Path traversal blocked: ${requestedFile}`);
}
return resolved;
}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/renderer/components/MarkdownRenderer.tsx (1)

323-332: ⚠️ Potential issue | 🟠 Major

Avoid silently swallowing URL conversion errors.
The empty catch hides unexpected issues; either remove the try/catch or report via Sentry and rethrow.

🔧 Suggested fix (remove silent catch)
-											// Attempt to convert non-standard URLs (e.g. git@host:user/repo)
-											try {
-												const converted = href.startsWith('git@')
-													? href.replace(/^git@/, 'https://').replace(/:([^/])/, '/$1').replace(/\.git$/, '')
-													: href;
-												if (/^https?:\/\//.test(converted)) {
-													window.maestro.shell.openExternal(converted);
-												}
-											} catch {
-												// Silently ignore unparseable URLs
-											}
+											// Attempt to convert non-standard URLs (e.g. git@host:user/repo)
+											const converted = href.startsWith('git@')
+												? href.replace(/^git@/, 'https://').replace(/:([^/])/, '/$1').replace(/\.git$/, '')
+												: href;
+											if (/^https?:\/\//.test(converted)) {
+												window.maestro.shell.openExternal(converted);
+											}

As per coding guidelines, "Do NOT silently swallow exceptions with try-catch-console.error blocks. Let unhandled exceptions bubble up to Sentry for error tracking."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/MarkdownRenderer.tsx` around lines 323 - 332, The
current try/catch around the git-to-https conversion swallows all errors
silently; remove the empty catch so errors from the conversion and the
window.maestro.shell.openExternal call can bubble up to your global error
handler (or, if you prefer explicit handling, catch the error, report it to
Sentry/telemetry with context including the href and the converted value, then
rethrow). Update the block that performs the href conversion (the ternary
conversion and the subsequent /^https?:\/\// check and call to
window.maestro.shell.openExternal) accordingly so no exceptions are discarded
silently.
🧹 Nitpick comments (2)
src/main/app-lifecycle/window-manager.ts (1)

162-193: Good security hardening with one consideration.

The navigation and permission restrictions are well-implemented:

  • Popup blocking prevents malicious window.open calls
  • Permission denial is appropriate for a terminal app
  • Navigation restriction in dev mode correctly allows same-origin dev server requests

Minor consideration: In production mode (line 181), the check allows any file:// URL, not just the app's own files. While the risk is low given sandbox mode and other restrictions, a more precise check could verify the URL starts with the app's file path:

♻️ Optional: Tighten file:// URL validation
 } else {
-	// In production, only allow file:// protocol (the built app)
-	if (parsedUrl.protocol === 'file:') return;
+	// In production, only allow file:// URLs within the app's renderer path
+	if (parsedUrl.protocol === 'file:' && url.startsWith(`file://${rendererPath.replace(/\\/g, '/')}`)) return;
 }

Note: This would require rendererPath to be accessible in scope (it's already available from deps).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/app-lifecycle/window-manager.ts` around lines 162 - 193, The
production navigation check in mainWindow.webContents.on('will-navigate')
currently allows any file:// URL; tighten it to only permit navigation to the
app's own renderer files by verifying the target URL starts with the app
renderer path (use the existing rendererPath from deps) instead of just checking
parsedUrl.protocol === 'file:'. Update the will-navigate handler to construct a
normalized file:// base (or compare paths after converting URL to file path)
using rendererPath and only return (allow) when parsedUrl.href or its resolved
filesystem path is within that rendererPath; otherwise call
event.preventDefault() and log as before.
src/main/ipc/handlers/system.ts (1)

193-208: Good security hardening for URL protocol validation.

The implementation correctly:

  1. Validates URL format before processing
  2. Restricts protocols to a safe allowlist (http:, https:, mailto:)
  3. Blocks dangerous schemes like file:, javascript:, and data:

The IIFE pattern for URL parsing (lines 199-205) is functional but unconventional. A simpler approach would be:

♻️ Optional: Simplify URL parsing
-	const parsed = (() => {
-		try {
-			return new URL(url);
-		} catch {
-			throw new Error(`Invalid URL: ${url}`);
-		}
-	})();
+	let parsed: URL;
+	try {
+		parsed = new URL(url);
+	} catch {
+		throw new Error(`Invalid URL: ${url}`);
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/ipc/handlers/system.ts` around lines 193 - 208, The URL parsing uses
an unnecessary IIFE; simplify the parsing in the
ipcMain.handle('shell:openExternal') handler by replacing the IIFE that assigns
parsed with a straightforward try/catch that constructs new URL(url) and assigns
to the existing parsed variable (or declares parsed) and throws the same Invalid
URL error on failure; keep ALLOWED_PROTOCOLS and subsequent protocol check
unchanged and ensure parsed.protocol is used afterward.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/renderer/public/splash.js`:
- Around line 7-21: Replace the direct assignments to window.onerror and
window.onunhandledrejection with event listeners registered via
window.addEventListener('error', ...) and
window.addEventListener('unhandledrejection', ...), update the handler callbacks
that reference progressText (preserve the same UI color/text behavior), and in
each handler call Sentry.captureException(...) with the error/event.reason (same
pattern used in src/renderer/main.tsx) in addition to console.error; ensure you
do not return false from the error handler so other listeners remain unaffected.

---

Outside diff comments:
In `@src/renderer/components/MarkdownRenderer.tsx`:
- Around line 323-332: The current try/catch around the git-to-https conversion
swallows all errors silently; remove the empty catch so errors from the
conversion and the window.maestro.shell.openExternal call can bubble up to your
global error handler (or, if you prefer explicit handling, catch the error,
report it to Sentry/telemetry with context including the href and the converted
value, then rethrow). Update the block that performs the href conversion (the
ternary conversion and the subsequent /^https?:\/\// check and call to
window.maestro.shell.openExternal) accordingly so no exceptions are discarded
silently.

---

Nitpick comments:
In `@src/main/app-lifecycle/window-manager.ts`:
- Around line 162-193: The production navigation check in
mainWindow.webContents.on('will-navigate') currently allows any file:// URL;
tighten it to only permit navigation to the app's own renderer files by
verifying the target URL starts with the app renderer path (use the existing
rendererPath from deps) instead of just checking parsedUrl.protocol === 'file:'.
Update the will-navigate handler to construct a normalized file:// base (or
compare paths after converting URL to file path) using rendererPath and only
return (allow) when parsedUrl.href or its resolved filesystem path is within
that rendererPath; otherwise call event.preventDefault() and log as before.

In `@src/main/ipc/handlers/system.ts`:
- Around line 193-208: The URL parsing uses an unnecessary IIFE; simplify the
parsing in the ipcMain.handle('shell:openExternal') handler by replacing the
IIFE that assigns parsed with a straightforward try/catch that constructs new
URL(url) and assigns to the existing parsed variable (or declares parsed) and
throws the same Invalid URL error on failure; keep ALLOWED_PROTOCOLS and
subsequent protocol check unchanged and ensure parsed.protocol is used
afterward.

Comment on lines +7 to +21
window.onerror = function(message, source, lineno, colno, error) {
if (progressText) {
progressText.style.color = '#ff6b6b';
progressText.textContent = 'Error: ' + (error?.message || message);
}
console.error('[Splash] Error:', message, source, lineno, colno, error);
return false;
};
window.onunhandledrejection = function(event) {
if (progressText) {
progressText.style.color = '#ff6b6b';
progressText.textContent = 'Error: ' + (event.reason?.message || String(event.reason));
}
console.error('[Splash] Unhandled rejection:', event.reason);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Search for Sentry utilities and error handling
rg -n "src/utils/sentry|captureException|captureMessage" --type=js --type=ts -A 2

Repository: RunMaestro/Maestro

Length of output: 11431


🏁 Script executed:

# Search for global error handler patterns
rg -n "window\\.onerror|onunhandledrejection|addEventListener.*error|addEventListener.*unhandledrejection" --type=js --type=ts -B 2 -A 2

Repository: RunMaestro/Maestro

Length of output: 1387


🏁 Script executed:

# Check the actual file and verify its contents
cat -n src/renderer/public/splash.js

Repository: RunMaestro/Maestro

Length of output: 2739


🏁 Script executed:

# Verify if src/utils/sentry exists
ls -la src/utils/sentry* 2>/dev/null || echo "File not found in src/utils/"
fd sentry src/

Repository: RunMaestro/Maestro

Length of output: 115


Use addEventListener for error handlers to avoid clobbering existing handlers, and report errors to Sentry.

Direct assignment of window.onerror and window.onunhandledrejection prevents other event listeners (like those set up in main.tsx) from being triggered. Additionally, errors are only logged to console and not reported to Sentry, violating the coding guideline to use Sentry for error tracking.

Replace the direct assignments with addEventListener and report errors via Sentry.captureException() (from the Sentry SDK, following the pattern in src/renderer/main.tsx):

🔧 Example refactor
-	window.onerror = function(message, source, lineno, colno, error) {
+	window.addEventListener('error', function(event) {
+		var error = event.error;
+		var message = event.message;
 		if (progressText) {
 			progressText.style.color = '#ff6b6b';
-			progressText.textContent = 'Error: ' + (error?.message || message);
+			progressText.textContent = 'Error: ' + (error?.message || message);
 		}
-		console.error('[Splash] Error:', message, source, lineno, colno, error);
-		return false;
-	};
-	window.onunhandledrejection = function(event) {
+		if (window.Sentry) window.Sentry.captureException(error ?? new Error(message));
+	});
+	window.addEventListener('unhandledrejection', function(event) {
 		if (progressText) {
 			progressText.style.color = '#ff6b6b';
 			progressText.textContent = 'Error: ' + (event.reason?.message || String(event.reason));
 		}
-		console.error('[Splash] Unhandled rejection:', event.reason);
-	};
+		if (window.Sentry) window.Sentry.captureException(event.reason);
+	});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
window.onerror = function(message, source, lineno, colno, error) {
if (progressText) {
progressText.style.color = '#ff6b6b';
progressText.textContent = 'Error: ' + (error?.message || message);
}
console.error('[Splash] Error:', message, source, lineno, colno, error);
return false;
};
window.onunhandledrejection = function(event) {
if (progressText) {
progressText.style.color = '#ff6b6b';
progressText.textContent = 'Error: ' + (event.reason?.message || String(event.reason));
}
console.error('[Splash] Unhandled rejection:', event.reason);
};
window.addEventListener('error', function(event) {
var error = event.error;
var message = event.message;
if (progressText) {
progressText.style.color = '#ff6b6b';
progressText.textContent = 'Error: ' + (error?.message || message);
}
if (window.Sentry) window.Sentry.captureException(error ?? new Error(message));
});
window.addEventListener('unhandledrejection', function(event) {
if (progressText) {
progressText.style.color = '#ff6b6b';
progressText.textContent = 'Error: ' + (event.reason?.message || String(event.reason));
}
if (window.Sentry) window.Sentry.captureException(event.reason);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/public/splash.js` around lines 7 - 21, Replace the direct
assignments to window.onerror and window.onunhandledrejection with event
listeners registered via window.addEventListener('error', ...) and
window.addEventListener('unhandledrejection', ...), update the handler callbacks
that reference progressText (preserve the same UI color/text behavior), and in
each handler call Sentry.captureException(...) with the error/event.reason (same
pattern used in src/renderer/main.tsx) in addition to console.error; ensure you
do not return false from the error handler so other listeners remain unaffected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments