ReqSSRF is a Req plugin for SSRF protection.
Add req_ssrf to your list of dependencies in mix.exs:
def deps do
[
{:req_ssrf, "~> 0.2.0"}
]
endReqSSRF runs on the currently supported Elixir versions and the compatible OTP versions. OTP 24 is not supported, because Finch uses an option that was introduced in OTP 25.
Versions older than the ones the CI matrix covers may still work, but they are not covered by CI and not officially supported.
A server that fetches a URL an outsider supplied can be pointed at the network it sits in rather than at the internet: another service, a database admin page, or the cloud provider's metadata endpoint. This is Server Side Request Forgery. For more information, refer to https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html.
Attach the plugin to the request rather than checking the URL yourself:
Req.new()
|> ReqSSRF.attach()
|> Req.get(url: submitted_url)ReqSSRF.attach/2 adds the check as a request step. Req follows redirects
by default, and it re-runs the request steps for each redirect, which means
that every hop is checked. Checking only the URL you were given is not enough:
a host that passes that check can respond with
302 Location: http://169.254.169.254/ to point the server at an internal
endpoint.
For a URL that may not be fetched, Req.request/2 returns
{:error, %ReqSSRF.BlockedError{}} and Req.request!/2 raises it. The error
names the URL and the reason.
Attach the plugin after any other plugin that can change the URL.
ReqSSRF.check/2 and ReqSSRF.allowed?/2 check a single URL. Only use them
when you are not making the request, for example if you are validating a URL
the user is saving. Check it again using ReqSSRF.attach/2 when you make an
HTTP request.
ReqSSRF.attach/2 takes five options:
:schemes- the accepted URL schemes. Defaults to["http", "https"].:allow_ip_address- whether a host written as an IP address is accepted. Defaults totrue.:deny- additional address ranges to refuse, as a list of CIDR strings, such as["10.0.0.0/8"]. Defaults to[].:resolver- the function used to resolve a name, for tests that must not touch the network. Defaults to&:inet.getaddrs/3, which is also the contract it has to mirror. There is no reason to set it in production.:timeout- how long to wait for a name to resolve, in milliseconds, or:infinity. Both address families are resolved at the same time, so this bounds the resolution as a whole. Defaults to2000.
They are stored under the single :ssrf_check request option. Pass
ssrf_check: false on a request to skip the check.
The check runs before the adapter, so it also runs when Req.Test has
replaced the adapter with a stub, and a stub on http://localhost is refused
like any other reserved address. Pass ssrf_check: false on those requests,
or give them a URL that resolves publicly.
If you validate a URL with ReqSSRF.check/2 outside a request, in a changeset
for example, then your tests resolve real names every time they build one.
Set the :resolver option to avoid this:
resolver = fn
_host, :inet, _timeout -> {:ok, [{8, 8, 8, 8}]}
_host, :inet6, _timeout -> {:error, :nxdomain}
end
ReqSSRF.check(url, resolver: resolver)- The scheme has to be
httporhttps, sofile:,gopher:and the rest are refused. Pass:schemesto set a different list, such as["https"]. - A host written as an IP address is matched against the IANA reserved ranges
in whatever notation it is written, so
127.0.0.1,2130706433,0177.0.0.1and[::ffff:127.0.0.1]are all refused. Passallow_ip_address: falseto refuse literal hosts outright. - A name is resolved with
:inet.getaddrs/3for both address families, and every address in the answer has to be one the internet can route to. - A host that does not resolve is refused.
- The check reads more addresses than the request uses.
Reqconnects over IPv4 unless you configure it otherwise, but the check reads the AAAA records too. A name whose A record is public and whose AAAA record is reserved is refused.
The addresses are checked, and then the host is resolved again when the
connection is made. A resolver under the attacker's control can answer
differently the second time, so a name that passed can still connect to a
reserved address. This is DNS rebinding. It needs a nameserver the attacker
controls, while the redirect that ReqSSRF.attach/2 closes needs only one
response header.
The library does not close it. Pinning the connection to the validated address
means passing the name as a connect option, and Req starts a Finch instance
per distinct set of connect options and never stops them, so a few thousand
submitted names would take the node down.
Using the check therefore means accepting that a host whose DNS the attacker controls can reach any address the server routes to. Deny egress to the private ranges and the metadata endpoint at the network if that is not acceptable. That also covers the fetches nobody remembered to check, which this library cannot.
The check mostly cannot tell a failed lookup from a missing record. A resolver
that refuses and a name that has no record of that family both come back as
:nxdomain, because OTP collapses every error into that one reason. So a
family whose lookup failed looks like a family with no addresses, and the check
rests on what the other family answered. A name whose A lookup fails and whose
AAAA record is public passes the check, and the request then connects over the
A record, which nothing checked. The mitigation is the same as for DNS
rebinding.
The exception is the :timeout, which the library imposes itself and can
therefore recognise. A lookup that misses that deadline is refused with
:resolution_failed rather than being read as a family with no records.
Nothing connects to a refused URL, so a user cannot find out what is listening on an internal address. Three things remain.
The first is the reason for a refusal. ReqSSRF.BlockedError says whether the
name failed to resolve or resolved to a reserved address, which tells a user
which names your resolver knows and a public one does not: inside a VPC,
submitting db.internal reveals whether it exists. Do not show the reason to
whoever supplied the URL.
The second is the fetch itself. A user learns little from a URL that passes, since they can reach a public host without your help, but the request goes out from your server's address rather than theirs. Yours is the address in the target's logs and the one blocked for abuse, and a service that allowlists your IP range will take a request from your server that it would refuse from the user. Rate limit the paths that fetch on a user's behalf.
The third is what the fetches leave behind. Req opens a pool per host and
never closes it. Set :pool_max_idle_time on your own Finch pool to retire
the idle ones.
Whether the server may fetch a URL and whether the browser may redirect to one
are different questions, and this library answers only the first. Do not use
ReqSSRF.check/2 to validate a redirect target.
Use SafeRedirect instead.