Pentesting Electron applications — how one XSS can escalate to full RCE — Defensys Innovations

What Is an Electron-Based Desktop Application?

Electron-based desktop applications are desktop apps that are built using web technologies like HTML, CSS, and JavaScript instead of traditional programming languages such as C++ or Java. A website normally runs inside your browser (Chrome, Edge, Firefox). An Electron app takes that website and packages it as a desktop application, so it looks and behaves like a normal app you install on your computer.

This is one of the main reasons Electron has become so popular. Many well-known applications, including Slack, Discord, Visual Studio Code, Figma, Notion, Microsoft Teams, and 1Password’s desktop client, are built using Electron. Instead of developing separate applications for Windows, macOS, and Linux, companies can reuse much of their existing web application code. This significantly reduces development time, simplifies maintenance, and allows them to deliver the same application experience across all major desktop platforms.

How Electron Apps Work

Electron Desktop Application

  🌐 HTML + CSS + JavaScript
              |
              ▼
  🖥 Chromium Renderer
    (Desktop User Interface)
              |
      IPC Communication
              |
              ▼
  ⚙ Electron Main Process
              |
              ▼
  🟢 Node.js Runtime
  📁 File System
  🖥 OS APIs
  🌐 Network
  ⚡ Child Processes
              |
              ▼
  🖥 Native Desktop Application

Every Electron application has four main parts that work together.

1. Main Process (The Brain)

The main process is the central controller of the application. It has full access to your computer, so it can:

  • Read and write files
  • Open folders
  • Start other programs
  • Access the network
  • Create application windows

Think of it as the manager of the app. It controls everything happening behind the scenes.

2. Renderer Process (The User Interface)

The renderer process is what you actually see on your screen. It is basically a Chrome browser tab running inside the application.

Its job is to:

  • Display buttons
  • Show menus
  • Render web pages
  • Handle user interactions

Unlike the main process, it should not be able to directly access your computer’s files or execute system commands.

Think of it as the front desk where users interact with the application.

3. Preload Script (The Security Guard)

The preload script sits between the renderer and the main process. Its job is to carefully decide what the renderer is allowed to access. Instead of giving the renderer complete control over the computer, the preload script only exposes specific safe functions.

For example, the renderer asks:

“Can I open this file?”

The preload script checks whether that action is allowed before forwarding the request. Think of the preload script as a security guard standing between the user interface and the operating system.

4. IPC (Inter-Process Communication)

Since the renderer cannot directly control the operating system, it needs a way to ask the main process for help. This communication happens through IPC (Inter-Process Communication).

For example:

Renderer
    |  "Please open this file."
    ▼
IPC Message
    |
    ▼
Main Process
    |
    ▼
Operating System

The renderer sends a message, and the main process performs the requested action if it is allowed.

Why This Matters (And Why Electron Apps Are Worth Testing)

In a web browser, JavaScript runs inside a sandbox, so even an XSS vulnerability is usually limited to the browser. Electron apps are different because they can interact with the operating system.

If Electron is configured securely, this access is well controlled. But if security features are misconfigured, a simple XSS can turn into Remote Code Execution (RCE), allowing an attacker to run commands, access files, or compromise the user’s system.

That’s what makes Electron applications a unique target for penetration testers. A small web vulnerability can have much bigger consequences than it would in a regular web application.

Reconnaissance: Understanding the Application

Before looking for vulnerabilities, spend some time understanding how the application is built.

First, find the Electron version. You can usually see it in Help → About or inside package.json or node_modules/electron/package.json. Also note the Chromium and Node.js versions because older versions may have known security issues.

Next, extract the .asar file, which contains most of the application’s JavaScript code.

npx asar extract app.asar output/

After extracting it, review files like main.js, preload.js, and package.json. You might find hardcoded API keys, authentication tokens, internal URLs, or debug code that was accidentally left behind.

Finally, check the Electron fuses. These are security settings built into the Electron binary during the build process and can’t be changed later. They control features like ELECTRON_RUN_AS_NODE, debugging options, and whether the app only loads code from the packaged app.asar.

npx @electron/fuses read --app /Applications/Foo.app

If fuses such as runAsNode or enableNodeCliInspectArguments are enabled when they shouldn’t be, an attacker may be able to run the application as a normal Node.js process and bypass many of Electron’s built-in security protections.

Static Analysis: Reviewing the Application Code

Static analysis means reviewing the application’s source code without actually running it. This helps you spot insecure settings and coding mistakes before you even start testing the application.

Check webPreferences in main.js

One of the first places to review is the webPreferences object in main.js. A few settings here have a huge impact on the application’s security.

SettingSecure ValueWhy it Matters
nodeIntegrationfalseStops renderer JavaScript from directly using Node.js APIs. If it’s enabled, even a simple XSS can turn into file access or Remote Code Execution (RCE).
contextIsolationtrueKeeps the web page separate from Electron’s privileged APIs and the preload script.
sandboxtrueRuns the renderer inside Chromium’s sandbox. Keep in mind that enabling nodeIntegration automatically disables the sandbox.
webSecuritytrueEnables the Same-Origin Policy and blocks unsafe cross-origin requests.
allowRunningInsecureContentfalsePrevents HTTPS pages from loading HTTP resources, reducing the risk of man-in-the-middle attacks.

If you’re testing an older Electron application, also check for the @electron/remote package. The built-in remote module was removed from Electron years ago, so it only matters if the application still includes this package.

Review the Preload Script

The preload script decides what the renderer is allowed to access. A common mistake is exposing generic IPC functions instead of only the actions the application actually needs.

Insecure example:

// Gives the renderer access to every IPC channel
contextBridge.exposeInMainWorld('api', {
  send: (channel, data) => ipcRenderer.send(channel, data),
  invoke: (channel, data) => ipcRenderer.invoke(channel, data)
});

The problem with this approach is that if an attacker gains control of the renderer (for example through XSS), they can call any IPC channel exposed by the main process.

A much safer approach is to expose only specific functions.

Safer example:

// Only exposes the actions the renderer needs
contextBridge.exposeInMainWorld('api', {
  readConfig: () => ipcRenderer.invoke('config:read'),
  saveFile: (path, contents) => ipcRenderer.invoke('file:save', path, contents)
});

This follows the principle of least privilege. Instead of giving the renderer full access, it can only perform the actions the developer has explicitly allowed.

Review IPC Handlers

Next, review every ipcMain.handle() and ipcMain.on() function.

These handlers receive requests from the renderer, so any user-controlled input should be treated as untrusted. Check whether the application validates input before using it in:

  • File operations
  • Shell or system commands
  • Database queries
  • Network requests

The main process has full access to the operating system. If an IPC handler blindly trusts user input, it can lead to serious issues such as command injection, path traversal, or arbitrary file access.

How to Test an Electron Application: A Complete Methodology

Now that we’ve covered the individual security checks, let’s put everything together into a practical testing workflow.

A typical Electron assessment starts with understanding how the application is built, then reviewing the source code, testing the application’s behavior, and finally checking OS-level security. The idea isn’t just to read the configuration and assume it’s secure—it’s to verify that those protections actually work.

1. Application Reconnaissance

Besides finding the Electron, Chromium, and Node.js versions and extracting the .asar archive, there are a few more things worth checking.

  • Frontend framework – Find out whether the renderer uses React, Vue, Angular, or plain JavaScript. This gives you a better idea of where to look for DOM-based XSS and other client-side issues.
  • Source maps – Check if .map files are included in the production build. If they are, you may be able to recover readable source code with comments and meaningful variable names.
  • Debug features – Look for leftover debug endpoints, verbose logs, or options like --remote-debugging-port that were accidentally left enabled in the release build.

2. BrowserWindow Security

Don’t just review the BrowserWindow configuration—test how every window behaves.

  • Check every BrowserWindow, not just the main one. Secondary windows like Settings, About, or Update pages are often less secure.
  • Test window.open() and make sure popups are blocked or only allowed for trusted websites.
  • Try navigating the application to an attacker-controlled website and check whether will-navigate or will-redirect prevents it.
  • Verify that Developer Tools can’t be opened in the production build using shortcuts, menu items, or command-line flags.

3. Preload Script Security

The preload script is the bridge between the renderer and the main process, so it’s worth reviewing carefully.

  • Review every contextBridge.exposeInMainWorld() call and understand what each exposed function can actually do.
  • Test for prototype pollution by passing values like __proto__ or constructor.prototype into functions that accept objects.
  • Check whether exposed APIs are protected with Object.freeze() so they can’t be modified by a compromised renderer.
  • Search for dangerous functions such as eval() and new Function(), since they run with higher privileges inside the preload script.

4. IPC Security

IPC is one of the most important parts of an Electron assessment because it’s how the renderer talks to the main process.

  • Map every IPC channel by reviewing ipcMain.handle(), ipcMain.on(), ipcRenderer.invoke(), and ipcRenderer.send().
  • Check whether the main process validates the sender before processing a request.
  • Make sure sensitive actions, such as file access or command execution, have proper authorization checks.
  • Fuzz every IPC handler with unexpected input and oversized payloads.
  • Test for race conditions by sending multiple requests or replaying previous IPC messages.

5. Node.js Integration & Command Execution

Even if nodeIntegration is disabled, verify it yourself.

  • Try accessing require(), process, window.process, or window.require from the renderer.
  • Review every use of child_process.exec(), execSync(), execFile(), and spawn() in the main process.
  • Test for command injection wherever user input reaches these functions through IPC or deep links.
  • If the application includes native .node modules, review them separately since they run outside the normal JavaScript sandbox.

6. Electron-Specific APIs

Electron provides several APIs that don’t exist in normal web applications, so they deserve their own review.

  • shell.openExternal() – Make sure untrusted input can’t control the URL, as this has led to real-world RCE issues.
  • shell.openPath() – Verify user input can’t be used to open arbitrary files.
  • dialog API – Ensure file dialogs can’t be triggered without user interaction.
  • clipboard API – Check that sensitive information isn’t copied to or read from the clipboard unnecessarily.
  • desktopCapturer – Verify screen sharing always requires user permission.
  • session API – Review cookie, permission, and session handling.

7. Web Application Security

Most Electron applications still rely on web technologies, so test for common web vulnerabilities as well.

  • Test for Reflected, Stored, and DOM XSS.
  • Check whether an XSS can be chained into Remote Code Execution (RCE) through exposed Electron APIs.
  • Review the application’s Content Security Policy (CSP).
  • Test backend APIs for common issues such as SQL Injection, IDOR, SSRF, CSRF, and XXE.

8. Navigation & Protocol Handling

Review how the application handles navigation and custom protocols.

  • Try navigating the application to untrusted websites.
  • Fuzz custom protocol handlers (such as myapp://) with unexpected input.
  • Review registerFileProtocol() for path traversal issues.

9. Filesystem Security

Look for ways to access or modify files outside the application’s intended directories.

  • Test for arbitrary file read/write and path traversal.
  • Check how temporary files are created and whether they’re properly protected.
  • Look for symlink attacks if the application writes files to user-controlled locations.

10. Authentication & Storage

Review how the application stores and protects sensitive data.

  • Ensure credentials and tokens are stored securely using safeStorage or the OS keychain.
  • Review session management, token expiration, and rotation.
  • Check localStorage and IndexedDB for sensitive data.
  • Verify cookie security flags and make sure logout properly clears the session.

11. Network Security

Inspect the application’s network traffic using the right tools for each protocol.

  • Intercept HTTP(S) traffic with Burp Suite or mitmproxy.
  • Test WebSockets if the application uses them.
  • Review any custom TCP or binary protocols.
  • Test certificate validation and check whether certificate pinning can be bypassed.
  • Verify the application respects proxy settings securely.

12. Update Mechanism

Finally, review how the application updates itself.

  • Ensure updates are downloaded over HTTPS from trusted servers.
  • Verify update packages are digitally signed before installation.
  • Test whether the application accepts older, vulnerable versions through downgrade attacks.

13. Sensitive Information Exposure

Look for sensitive data that may have been left behind during development.

  • Search the extracted source for API keys, tokens, secrets, and environment variables.
  • Review logs and crash dumps for credentials, session tokens, or other sensitive information.

14. Binary & Package Analysis

Review the packaged application for security weaknesses.

  • Run strings analysis to find hardcoded secrets or internal URLs.
  • Check that debug symbols have been removed from the release build.
  • On Windows, test for possible DLL hijacking.
  • Modify and repack the .asar archive to verify the application detects tampering.

15. Operating System Integration

Review how the application interacts with the operating system.

  • Check startup and persistence entries added during installation.
  • Review file and directory permissions to ensure local users can’t modify the application or its data.

16. Stability & Abuse Testing

While these are usually lower severity, they’re still worth testing.

  • Crash the renderer and main process to see how the application recovers.
  • Test resource exhaustion by sending oversized files or excessive IPC/network requests.

17. Logging & Monitoring

Finally, review the application’s logging behavior.

  • Make sure important security events are logged.
  • Check that error messages don’t expose stack traces or internal paths.
  • Verify debug logging is disabled in production builds.

Supporting Reference: Content Security Policy (CSP)

A good starting point for an Electron renderer is:

Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none';

If the application has no CSP or allows directives like 'unsafe-inline' or 'unsafe-eval', it’s worth investigating since these settings weaken protection against XSS attacks.

Supporting Reference: Certificate Pinning Bypass (Frida)

If the application uses certificate pinning, you’ll first need to identify where the certificate validation is implemented before attempting to bypass it. Depending on the application, this could be in Chromium’s networking stack, a native Node.js addon, or another bundled native library. Unlike mobile applications, Electron applications do not have a universal certificate pinning implementation, so there is no single Frida hook that works for every target. The functions that need to be hooked depend entirely on how the application performs certificate validation. Tools such as frida-trace and reverse engineering can help identify the relevant functions before attempting a bypass.

Useful Tools

The following tools can make Electron application testing much easier, whether you’re reviewing the source code or testing the application at runtime.

  • Electronegativity — Static scanner (AST/DOM-based) purpose-built for Electron misconfigurations like nodeIntegration: true or contextIsolation: false. Still actively maintained by Doyensec.
  • @electron/fuses (CLI) — Read the fuse configuration of any packaged app (npx @electron/fuses read --app <path>) to check build-time hardening independent of runtime config.
  • ASAR — Extract .asar archives for source review.
  • Burp Suite / mitmproxy — HTTP(S) interception and backend API testing.
  • wsrepl / ZAP — WebSocket-specific traffic inspection where Burp falls short.
  • Wireshark / mitm_relay — Non-HTTP and raw TCP protocol capture and tampering.
  • Frida — Runtime instrumentation: hooking function calls, bypassing certificate pinning, inspecting internal logic.
  • grep / Semgrep — Fast pattern search across extracted source for eval(), shell.openExternal(), insecure webPreferences, and hardcoded secrets.

Real-World Example: Insecure Use of shell.openExternal()

The shell.openExternal() API is used to open a URL in the user’s default browser or another application registered to handle a specific protocol. It is commonly used in Electron applications for opening documentation, help pages, or external websites.

The API itself is not inherently vulnerable. The security risk arises when an application passes untrusted user input directly to shell.openExternal() without validating it first. If an attacker can control the URL being opened, they may be able to trigger unintended behavior or abuse protocol handlers installed on the system.

Insecure Implementation

The following example directly passes user-controlled input to shell.openExternal():

// User-controlled input passed directly to shell.openExternal()
const userInput = "https://evil.com"; // Could come from IPC
shell.openExternal(userInput);

If userInput originates from an untrusted source—such as an IPC message, user input, or remote content—an attacker may be able to supply arbitrary URLs or protocols.

Potential Attack Scenarios

Without proper validation, an attacker may attempt to:

  • Open local files using the file:// protocol.
  • Abuse custom URI schemes registered by other applications.
  • Trigger malicious deep links through user-controlled input.
  • Exploit weak IPC validation to make the main process perform unintended actions.

While the impact depends on the operating system and the target application, allowing arbitrary URLs to be opened can become part of a larger attack chain.

Secure Implementation

A secure implementation validates the URL before passing it to shell.openExternal(), ensuring that only expected protocols are allowed.

const userInput = "https://example.com";

try {
  const parsedUrl = new URL(userInput);

  // Only allow HTTP and HTTPS URLs
  if (!['http:', 'https:'].includes(parsedUrl.protocol)) {
    console.error('Invalid protocol');
    return;
  }

  shell.openExternal(userInput);
} catch (error) {
  console.error('Invalid URL:', error);
}

By restricting the allowed protocols to http:// and https://, the application prevents potentially dangerous protocols from being opened.

Dangerous IPC Pattern

One of the most common mistakes is exposing shell.openExternal() through an IPC handler without validating the received URL.

// IPC handler with no validation
ipcMain.handle('open-url', (event, url) => {
  shell.openExternal(url);
});

In this example, the main process blindly trusts the renderer. If the renderer becomes compromised—for example, through a Cross-Site Scripting (XSS) vulnerability—an attacker can instruct the main process to open arbitrary URLs or protocol handlers.

Key Takeaways

When testing Electron applications, remember that shell.openExternal() is not the vulnerability. The real issue is trusting unvalidated user input.

During a security assessment, verify that:

  • Only trusted URLs are passed to shell.openExternal().
  • The application restricts allowed protocols (for example, http:// and https://).
  • IPC handlers validate and sanitize all user-controlled input before invoking shell.openExternal().
  • Renderer processes cannot instruct the main process to open arbitrary URLs or protocol handlers.

Proper input validation and secure IPC design significantly reduce the risk of attackers abusing shell.openExternal() as part of a larger exploit chain.

Mitigations: Securing Electron Applications

Most Electron vulnerabilities can be prevented by following a few security best practices:

  • Set nodeIntegration: false, contextIsolation: true, and sandbox: true for every BrowserWindow.
  • Keep the preload script minimal — expose only named, specific functions through contextBridge, never raw ipcRenderer methods.
  • Validate and sanitize all IPC input before it’s used for privileged operations.
  • Apply a strict CSP without 'unsafe-inline' or 'unsafe-eval'.
  • Flip the relevant Electron fuses at build time — disable runAsNode and enableNodeCliInspectArguments unless the app genuinely depends on them, and enable onlyLoadAppFromAsar plus enableEmbeddedAsarIntegrityValidation.
  • Keep Electron, Chromium, and Node.js current to pick up upstream security patches.
  • Ship updates over HTTPS, verified with digital signatures.
  • Never store secrets in source or inside the .asar archive — they’re trivially extractable.

Conclusion

Electron applications combine the flexibility of web technologies with the power of native desktop applications, making them a unique target for security testing. While many of the vulnerabilities you’ll encounter such as XSS, insecure APIs, or injection flaws are familiar from web application testing, their impact can be far greater if Electron’s security features are misconfigured. In the worst cases, a simple web vulnerability can escalate into full Remote Code Execution (RCE).

The good news is that Electron applications are relatively transparent. Since most of their code is packaged as JavaScript inside an easily extractable .asar archive, penetration testers can learn a great deal through static analysis before even running the application.

As Electron continues to power popular desktop applications, understanding its architecture and common security pitfalls is becoming an increasingly valuable skill for penetration testers. By combining traditional web security techniques with Electron-specific testing, you can uncover vulnerabilities that might otherwise go unnoticed.

← Back to Blog