garrison

garrison

If you want to perform arbitrary external HTTP requests you have to be very careful to avoid Server-Side Request Forgery (SSRF). Certain IPs may respond with privileged data (e.g. cloud servers often have a special metadata endpoint). There could also be privileged internal services that a server has access to, some of which may use HTTP.

The Phoenix Security Guide mentions this but provides no guidance beyond “avoid making requests with user input”. This Paraxial guide provides similar advice (I suspect the former adapted the latter).

This is unhelpful; there are entire classes of applications which need this behavior, such as: anything which renders link previews, proxies images, reads RSS feeds, fetches web content, and so on. This is not a niche use-case.

As a start you can attempt to filter out malicious user input (e.g. a link to a private IP address), but this is insufficient as a DNS response can still point a malicious domain to a sensitive address. I have seen this referred to as “DNS rebinding” but I think this is an abuse of terminology (though there is overlap).

It is tempting to think you can get around this by first resolving the name with :inet_res and then connecting to it as normal, but this is also insufficient. There is no guarantee the connection will receive the same DNS response. An attacker could simply toggle back and forth until they get lucky.

We can find the correct solution in go-camo, an image proxy written in Go. One must first resolve the name, check the address, and then make a request to that address. It seems that in Go this can be done via some callback (I don’t know Go).

So back in Elixir, we can resolve the name with :inet_res but we need to pass the resolved IP directly into our HTTP client. The problem is that we still need to pass in the original hostname, not only for the Host header but more importantly for HTTPS. The hostname is needed to validate the cert.

Mint’s connect/4 actually supports this via a hostname option, but as far as I can tell Finch and therefore Req do not take advantage of this. Finch allows conn_opts but only when you start a pool, which is unhelpful. As a result, I don’t see any viable way to protect against SSRF in the application layer using our standard tools (Finch/Req).

This seems like a problem. Am I missing anything?

Showing Posts 1 to 4

LostKobrakai

LostKobrakai

From a quick look the custom :hostname option seems to exist because of this:

https://github.com/elixir-mint/mint/blob/8f95bc73c13ce1ae058e0c69f035d8d345f96fe9/lib/mint/core/transport/ssl.ex#L475-L477

On newer OTP versions it might be safe to manually set the Host header and use the IP on the address.

garrison

garrison OP

A blame shows that the option was added in this PR which adds support for connecting to IPs (and sockets). The hostname is actually required when connecting to an IP. (Actually, what are you supposed to do if you’re really connecting to an IP, pass it in as a string?)

The docs claim the hostname value is used for the Host header and for HTTPS: to validate the cert and also for SNI (to request the right cert). It also says “and so on”, whatever that means lol.

This is all fine and good; it’s exactly what I want to do. Resolve the domain myself and then connect to the IP directly (to avoid malicious DNS replies) while preserving the original host for HTTPS.

The problem is that as far as I can tell Finch (and therefore Req) provides no way to pass this value in. Apparently it can be passed in with conn_opts when creating the pool, but I want to pass it in when making the request. I have a very poor understanding of how Finch’s pools actually work so maybe there actually is some way to do this? But it’s certainly not obvious, and this is a serious security issue for which we need to provide a solution and clear guidance.

Are you suggesting the Host header will be used for HTTPS by Mint when connecting to an IP? I haven’t found anything to this effect in the docs or code but I also didn’t try it either.

As far as I can tell the code you linked is related to Mint verifying cert hostnames itself because they don’t like the OTP behavior or it was not previously available (not immediately clear to me).

joram

joram

You can pass the hostname to Req as part of connect_options:

Req.get(
  "https://123.456.789.123",
  connect_options: [hostname: "www.example.com"]
)

Req will dynamically start or reuse a Finch pool with those options.

Pools don’t shut down by default so you may also want to specify a pool_max_idle_time, and you probably also want to disable redirects:

Req.get(
  "https://123.456.789.123",
  connect_options: [hostname: "not.actually.example.com"],
  pool_max_idle_time: :timer.minutes(5),
  redirect: false
)
garrison

garrison OP

Thank you, this is exactly what I was looking for. I was preoccupied with Finch and didn’t spend enough time looking through Req’s APIs. In hindsight it makes sense that Req starts the pool itself.

Following redirects is desirable in many cases. It would probably be better for Req to support this IP filtering pattern natively. I’ll see about writing a proposal for that.

— All posts loaded —

Where Next? Top

Trending in Questions Top

RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
nseaSeb
Hello, I know there is an approach for handling lists that allows for optimized traversal, but I can’t recall the specific method (somet...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
brecabral
Documentation While reading the Scoped Routes section, I noticed that the documentation currently refers to a problem without explainin...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
webofbits
With AI doing more of the implementation work, I’ve been wondering how much coding I should deliberately keep doing myself. My main conc...
#ai
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews