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")
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.
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.
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!