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

itzmidinesh
Hi all, I’m working on a Phoenix project where I need to make external HTTP requests and I’m trying to choose between Req and HTTPoison....
New
yarrichar
I’m very new to Elixir, so sorry if this is a silly question: I am wondering what happens to the mailbox of a process when the machine i...
New
pragdave
How do I tell Hologram not to escape HTML when substituting into templates? {Textile.render(@product.long_description)} escapes the HTM...
New
Fl4m3Ph03n1x
Background When trying to execute mix release on a Windows 11 machine for a Phoenix project I get the following error: * assembling mark...
New
rlipscombe
I’d like to create a file with 600 permissions (rw-------), but there’s a race condition if I create it and then change the permissions. ...
New
saleyn
Is there a way to import record definitions in the iex shell for pretty printing (similar to the rr command in the erl shell)? This woul...
New
rlipscombe
When I use Mix.install with a git dependency, using git@github.com:... how do I change the SSH identity used? I’ve got two Github accoun...
New

Other Trending Topics Top

dominicletz
Hey there, I haven’t seen a list of useful Elixir specific skills for the popular AI tools yet so I’m starting one here with my first. ...
New
raa-ziq
Hi everyone :waving_hand: I wanted to share something I’ve been building — NowBlind, a random chat app designed to help people discover ...
New
mudasobwa
I fully migrated to my own harness from Anthropic/Gemini and I think it’s time to share it. Welcome DSH, the DeepSeek Harness, fully writ...
New
KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
OndrejValenta
(I just needed to vent somewhere and LinkedIn is full of hope, or hype, I’m not sure which exactly) AI dream has many faces, but general...
New
kip
Localize is the next generation localisation library for Elixir. Think of it as ex_cldr version 3.0. The first version will be released ...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews