Code Code Ship logo
 

Shun keeps your HTTP secrets safe

February 13, 2023

Evadne Wu (whom many of you know from her talks at ElixirConf and PC builds on Twitter), released Shun to the world in April 2022. Judging by the number of stars on the repository and the download stats on Hex, shun has gone mostly unnoticed by the Elixir community.

Shun is the library that you didn't know you needed. So what is it?

I'll start by looking up the word "shun" in the dictionary. Cambridge Online says that "shun" is a verb that means "to avoid something". I think you'll agree later, once you've read this blog post in its entirety, that Evadne did a great job of naming this library.

Shun describes itself as a library that "provides URI, IPv4 and IPv6 address verification primitives for Elixir applications." Let's take a look at some sample code. I've lifted this right from the README:

defmodule MyApp.Shun do
  use Shun.Builder
  handle Shun.Preset.IPv6.Embedded
  reject Shun.Preset.AWS.InstanceMetadata
  reject "10.0.0.0/8"
  reject %URI{scheme: scheme} when scheme != "https"
end

What can we learn from this? Shun can "handle" things. Shun can "reject" things. "Things" includes IP addresses, "presets" (more on this later), or even URIs that don't use https. Shun has some kind of a DSL to hopefully make this easier. It looks like we are defining some kind of pipeline here to validate URIs.

We can use our freshly-built validation module like this:

Shun.verify(MyApp.Shun, "http://google.com")

Preventing leaks

By now you should have a foggy idea of what shun does, so now I'm going to talk about a real-life scenario that shun helps you protect against.

If you run your application in the cloud, then it's likely that your application has access to some magic URLs that provide information about the server it is running on. If your server runs on AWS EC2 for example, you can run an HTTP GET to the following address: http://169.254.169.254/latest/meta-data/ to get all kinds of information related to the instance it is being run on. This can result in your IAM credentials being leaked which we would obviously like to avoid. Amazon's docs encourages restricting access, and gives instructions on doing so using iptables or firewall settings.

But if your application is doing HTTP requests to URLs supplied by your users, then you may wish to restrict URLs to disallow an HTTP GET on AWS instance metadata URLs.

Now you might be thinking "how hard can it be?". So you write the following code:

defp check_url(url) do
  url = URI.parse(url)
  if url.host == "169.254.169.254" do
    {:error, "URL not allowed"}
  else
    :ok
  end
end

But it's not so simple. An attacker can construct a DNS record to resolve to your AWS instance metadata IP, and circumvent your check. shun has got you covered though, because it _resolves all domains with DNS_ to prevent this kind of trickery.

Shun comes packaged with some handy presets to help you out here. You can pick and choose, but probably every app should start by just rejecting _all_ of these.

Other cloud platforms have other ways of accessing similar metadata. If you decide to use shun, you should investigate how your own cloud provider does this, and send a PR to shun to add a preset.

How does it work?

It took me a minute to really grok how shun works. There are 3 types of rules, and 3 types of targets, and you have to parse the README carefully to get a good grasp on how it works.

The 3 types of rules are accept, reject, and handle. Accept and reject are self-explanatory, causing a matching target to be either accepted or rejected. handle allows you to specify a module or a function that will be called as a handler with the target as only argument, returning either :accept or :reject as appropriate.

Shun evaluates your rules from top to bottom, and stops when it finds a rule that matches.

The 3 types of targets are: URI structs (%URI{}), :inet tuples (IPv4: {127, 0, 0, 1} and IPv6: {0,0,0,0,0,0,0,1}), or CIDR ranges ("10.0.0.0/8").

Shun also defines some fallback rules at the bottom of your validator module that reject IPv4 and IPv6 addresses, amounting to reject {_, _, _, _} and reject {_, _, _, _, _, _, _, _} and a third fallback rule that is slightly more complicated: URIs are resolved to an IP address, and processed again beginning with the first rule. URIs are "only accepted if all underlying IPs are accepted", so in the case of DNS round robin, _all_ the addresses must validate for the URI to be accepted.

I think of shun as having two layers: the bottom layer and foundation for the library is the IP address handling. The top layer, which provides convenience and flexibility, and falls back gracefully to the bottom layer, handles %URI{} structs.

Conclusion

Shun gets 5 ⭐ from me for providing an easy way to lock down your application and prevent unwanted access (or allow only wanted access) to URLs and IP addresses, and for its comprehensive approach that catches things that most of us will miss when implementing our own URL checking.

If you are accepting URIs as user input, you should consider switching to shun to benefit from all the thought and effort that Evadne has put into this project.

If you got this far, head on over to GitHub and give shun a star ⭐. And if you see something in the docs or the README that could be improved, I'm sure Evadne would love a PR!

By Derek Kraan
Share
Code Code Ship helps you sell your library on a subscription basis.
Read more
Related articles
2023-04-19T07:00:00Z
Parker Selbert, Creator of Oban
Parker pulls back the cover on what it's like to commercialize your package
2023-09-07T11:30:00Z
Daniel Cazzulino, creator of Moq and SponsorLink
Daniel tells us about his future plans for SponsorLink
Code Code Ship logo

Get in touch

You can get in touch with us via email or on our Discord server. Click the button for an invite.

Subscribe

Always stay up to date by subscribing to our newsletter.