A Deep Dive of CVE-2022-33987 (Got allows a redirect to a UNIX socket)
Every week, almost without fail, I come across one thing that confuses, entertains, or most commonly infuriates me. I’ve decided to keep a record of my adventures.
I am subscribed to security releases for several PHP projects. I do this not because PHP projects tend to have many weaknesses (okay, they might), but rather because they are some of the most reviewed code out there. So last week when I saw MediaWiki publish a new security release, I wanted to take a more in-depth look.
The Background
In a normal environment, identifying security fixes between versions can be quickly done using Github's branch/tag comparison feature. This effort was helped by MediaWiki keeping great project information on their Phabricator. Going through the commits within the 2.81.4 release I noticed that the changes roughly fell into three camps:
1) Preparing for PHP 9's deprecation of null types passed to built-in functions expecting strings (for more detail see this blog)
2) Dependency updates (some of which were security updates, which is what we care about)
3) Fixing some performance issues with recent change
Like any good project, updating vulnerable dependencies is critical to the security story and MediaWiki seems to do an okay job of this (although the vulnerability patched in this release was patched upstream on May 25th, 2022). Looking through the commits we see the following message on a few:
Updating got to 11.8.5 * GHSA-pfrx-2q88-qq97
Interesting, MediaWiki skins and extensions were running vulnerable versions of 'got', a Node HTTP request library. I had mixed feelings when I realized that the security release of a PHP project was due to a Node security issue, but onwards I went. The following were the submodules with bumped got versions.
skins/MinervaNeue extensions/Math extensions/AbuseFilter
The Problem
So, what is got doing that is so bad? It turns out that in a somewhat log4j reminiscent issue, the issue was scheme handling of a somewhat obscure scheme.
Back in September 2015 in version 4.1.1, got added support for making requests via Unix domain sockets. The purpose outlined was simple, users may want to make requests to sockets running on the local server. This is not all too different from making requests to localhost, however, Unix sockets tend to run more powerful IPC tooling, and the use-cases provided were 'docker' and 'fleet', which gives you some idea.
Turns out they ended up adding support for both the Unix scheme: 'unix:/' and an HTTP Unix scheme: 'http://unix:/'. They borrowed the latter formatting from the now deprecated request framework (More on this in a bit). Interestingly, using the direct 'unix:/' scheme wasn't actually working, at least in 11.8.3, where a dependency, http-timer was not able to parse this URL format (oops):
at origin.emit (node_modules/@szmarczak/http-timer/dist/source/index.js:43:20) code: 'ERR_INVALID_PROTOCOL'
The crux of the issue isn't making Unix socket requests, instead it was the nature of how redirects occurred. If a malicious user was able to provide a URL to a vulnerable application, they could provide a URL that resolved to a 30x redirect that ended up at a unix:// socket location. Making requests to the localhost or local network is pretty much the definition of a Server Site Request Forgery (#SSRF). Normally, we'd expect a developer to filter such requests, the issue here was that there was no way for the standard developer to prevent this scheme from being used, as even if they were initially validating the URL, got was handling the redirect automatically.
Interestingly, got suggests a different library to add SSRF protection, got-ssrf. This would have ended up preventing the issue because the `unix` part of the URL will end up becoming the URL's hostname and got-ssrf will try, and fail, to resolve that so it returns:
RequestError: getaddrinfo ENOTFOUND unix
It's at this point that we wanna look at what the user can do with a Unix socket. The most obvious case is services like Docker but you can also check if files exists sometimes. Local users are typically able to communicate directly with the Docker socket using something similar to the following:
curl --silent -XGET --unix-socket /var/run/docker.sock http://localhost/version
This is equivalent to the following in got:
got('http://unix:/var/run/docker.sock:/version')
The previous example is pretty contrived but the Docker management API is far more powerful, given the request method (GET, POST, etc.), it supports things like creating a container (POST), killing a container (POST), checking auth configuration (POST), executing commands in a running container (POST), Getting container logs (GET), Export a container or images (GET), and much more.
To exacerbate matters, in got redirects are followed for many HTTP status codes, so attackers can use a 307 or 308 to pass through a POST request and its content to a Unix socket without rewriting it to a GET request, if the vulnerable application is crafting a POST request that is.
I mentioned earlier that got borrowed the http://unix/ formatting from the now deprecated request framework. I would have been remiss if I didn't look at the 'request' framework to see if the same vulnerability occurs. As you might expect, yeah, it is also vulnerable. Strictly speaking this should also be a distinct CVE, but because request is deprecated, they would be unlikely to respond. Interestingly, there is a known issue, requiring a host header to be set, that prevents this from working in more cases. Small victories I suppose.
import request from 'request' // http://127.0.0.1:81 redirects with a 307 to http://unix:/var/run/docker.sock:/version request({ uri: 'http://127.0.0.1:81', headers: { 'host' : "anything" }, }, function (error, response, body) { console.log('body:', body); });
The Solution
For most folks, the solution is to update got to version 11.8.4 or greater. Additionally, if accepting untrusted URLs for resolution, developers need to be cognizant of SSRF and perform URL sanitization/validation to ensure that they aren't resolving sensitive URLs.
The solution from the folks at got is pretty straight forward. The developers simply check to ensure that redirect URLs that have the Unix Sockets are blocked unless the initialized URL was also a Unix Socket. They are additionally proposing defaulting an option, EnableUnixSockets, to false to disable Unix domains entirely by default.
As for MediaWiki, turns out they were only using got seemingly as part of their test suites, so for the most part they were unaffected, but it’s good that they updated their dependencies anyway. Unfortunately, some of the modules they updated ALSO rely on request, which is both deprecated and as I showed, vulnerable. Wow, long walk for that conclusion, huh?