Allowing Outbound Net Requests from a Dropserver App
With Deno 2.0 delayed again I recently tried to implement outbound net requests for Dropserver apps using Deno v1’s permission model. I was excited to offer this new capability for Dropserver apps but unfortunately things did not go as I had hoped.
Problem Description
In the current version of Dropserver an app is unable to make a dynamic request to another host. It’s blocked by the Deno sandbox: there is no --allow-net
permission.
(NB: Static requests are allowed by Deno. A static request is one where the URL is hard-coded into the source code files, like a typical import * from 'some-url.com';
statement. When I talk about allowing outbound net requests I mean the dynamic type.)
Here are some use cases for outbound net requests:
- An app needs access to a finite set of hosts, determined by the developer. Example: a dice roll app hits random.org for an integer.
- An app that needs dynamic access to hosts that are directly decided by the user. For example a RSS reader. There is a 1-1 relation between the user subscribing to a feed and the app needing access to a new host.
- An app that needs open ended access to the internet. For example a Fediverse server or a web crawler. One can not expect the user to approve every domain.
Regardless of which hosts are allowed and how, here are some things I want to guard against for the safety of the admin and users:
- HTTP(S) only. No telneting around to see what services can be discovered.
- Dial only. No listening! A dropserver app can not create listeners, that’s
ds-host
’s job. (Amazingly the Deno permissions don’t make that distinction.) - SSRF protection: no requests to the local network unless it’s explicitly allowed by the ds-host admin.
Also I’d like to log all requests, with at least the name of the host contacted (see below).
The most interesting apps need open-ended access to the net (3rd bullet above), so my goal was to solve that case.
Deno Permissions
Dropserver apps run in Deno because of its sandboxing properties. See this bit of history to know how we ended here. It’s an effective sandbox for limiting file read/write and execution access. But on the networking side, it’s far from great. Let’s look at how it works:
Permissions for the network come in the form --allow-net
and --deny-net
. So far so good. However, here are the gotchas:
--allow net
permits outbound requests (dial) and listen! So if you naively rant all net access because you have an app that may want to reach any host (situation three above), that app can now create listeners (servers) on any port (within operating system restrictions).- The
--allow-net
and--deny-net
are compared with the URL that is requested, not the resolved IP.
These two issues open a lot of potential security problems if I open net access for Dropserver apps.
Good to know: Deno uses reqwest
Deno uses reqwest
internally (for now at least) to make requests on behalf of the running code. This is good to know because we can assume that Deno won’t implement something that is not part of reqwest
. Currently there is an open issue for some sort of SSRF protection – it’s been open for 3 years. So I can’t expect that to happen.
Reqwest
now allows for a custom DNS resolver which would help with SSRF protection. Interestingly this feature was requested by Ry, the creator of Deno, so I suspect they use it internally, but so far it’s not exposed in the CLI as far as I can see.
The HTTP_PROXY
One feature of reqwest
that Deno exposes is the ability to declare a proxy for HTTP requests via environment variables like HTTP_PROXY
. This is a good way to route requests through a proxy that Dropserver controls to prevent any unwanted request. Proxying requests would allow me to “see” and filter to my heart’s content. I could even reuse Dropserver’s own SSRF mitigation code. It’s the ultimate solution.
Unfortunately, while the proxy works as expected, it in no way prevents an app from making requests that bypass the proxy. Basically the proxy is only used for “fetch” requests. You can craft a request from low-level APIs, and I think you can even override the proxy using the fetch API.
The proxy is merely a “helpful suggestion”, so not usable as a security boundary. (That’s not a knock against the proxy implementation: a proxy, by itself, is not meant to be a security boundary.)
A Hail Mary: –allow-net-fetch
If only Deno could block all net access except those that go through the proxy, then I’d be set. I posted a request for --allow-net-fetch
permission in the Deno issues hoping they find it an interesting approach. Since Deno 2 is still in the works, I am hoping there is still time to slip that in, if they haven’t already done something similar.
Non-Deno Solutions
I could run Deno inside a Linux namespace that gives me full control over the network. There are a few problems with that:
- this only works in Linux. This is unfortunate because I am leaning towards making Dropserver installable on Mac and Windows natively to reach the broadest possible audience.
- I currently use Bubblewrap to further isolate Deno sandboxes when
ds-host
runs in a Linux environment but its net namespace support is on/off. Setting up the virtual networking elements to get a functioning network is not easy. And Bubblewrap doesn’t even run in Docker, or at least not easily.
So limiting an app’s net access to an acceptable degree without Deno’s help is complicated and will take a lot of my dev time, and it will only work on Linux at a time when I am eager to not limit Dropserver to Linux! So that’s a nope.
Report Hosts to User
While I am talking a lot about permissions, visibility into what hosts are being contacted and how often is an important feature for a user who is concerned about what an app is doing.
There are good reasons for an app to request open-ended access to the web as we’ve seen above. But how can a user be made to feel comfortable about an app’s behavior after they’ve granted that permission? The key is to give them visibility into what the app is actually doing with that permission.
If the user can see reports that point out the most contacted domains, the payload sizes, and other metrics that might reveal whether an app is behaving correctly or not.
I’ve always felt that it was weird when a system asks me to grant some permission but then offers zero information about the usage of that permission. If it’s so important to my safety that I have to grant a permission, why are you not showing me what is done with that permission?
With this in mind I worked on a logging proxy for outbound requests from the appspace sandbox.
Proxying outbound requests
I implemented a proxy using goproxy (see commit e78d447).
I learned along the way that MITM’ing the connection is very computationally expensive and slows down requests significantly when there are many at a time. With no MITM the performance is OK.
With no MITM the proxy only sees the host and not the full path, or the payload, but I can at least report that host X was contacted.
Maybe I’ll add an option to turn on MITM for an appspace. The user might do that if they get suspicious of an app’s activity.
Conclusion
After getting the proxy to workable condition I achieved 0.5 of the four requirements I laid out at the top. I only managed to log hosts and only if the app uses the Fetch API without defeating the proxy.
The other requirements (HTTP-only, No listening, and SSRF protection) I am not able to pursue without better tools from Deno, or I have to make Dropserver Linux-only and dive into Linux namespaces and veth
s galore.
What if Deno 2 doesn’t solve my issues? This would be bad, but I’m a crafty guy and I’ll figure something out.