I learned something today and wanted to share.
There are ranges of IP addresses that mean special things, some refer to:
- yourself (localhost)
- other machines on your local network (like other computers you own inside the same datacenter)
- special networking services (provided by AWS or whomever)
- DNS turns domain names into IP addresses
- normally, DNS is highly static: always responds the same and caches hard to keep the web fast and efficient
- tricky DNS resolvers break this norm, resolving in different ways with short TTLs
rbndr.us is an example of a tricky one I just learned about. It uses a DNS resolver that automatically oscillates its IP address resolution between the subdomains you give it. So if you resolve http://a9fea9fe.01010101.rbndr.us it will return one of 2 IP address (specified by a9fea9fe and 01010101).
This is easily verified with the dig utility:
# dig +short DOMAIN - quick ip address resolution
$ dig +short a9fea9fe.01010101.rbndr.us
1.1.1.1 # benign IP address
$ dig +short a9fea9fe.01010101.rbndr.us
169.254.169.254 # dangerous IP addressWhen we make an HTTP requests to a domain name, it performs a DNS resolution to an IP address automatically and invisibly.
Executing webhooks for end-users is dangerous because:
- you make an http request (these have many gotchas)
- ...from inside your infrastructure (your user/server can see privileged information)
- ...to a user-provided address to request (never trust the user)
So when a user gives you a URI to post to, naturally you want to go ahead and resolve that in advance and check what comes back against a list of bad addresses. Easy enough, what's the problem?
If you verify DNS points to a safe IP address as a separate process from when you make the HTTP request, you can be attacked by nefarious DNS resolvers, which are trivially used by your end-users.
Code that looked safe to me:
def send_request_safely url, options
unless validate_url(url)
raise "naw, dawg"
end
# ...milliseconds later...
response = HTTParty.get(url, options)
show_to_user(response)
endSeems fine, right? We validate the final destination before we make the request on every request.
Unsafe code explained:
def send_request_safely url, options
# resolve DNS and check block list
unless validate_url(url)
raise "naw, dawg"
end
# ...milliseconds later...
response = HTTParty.get(url, options) # resolves DNS all over again... oops
show_to_user(response) # pwned!
endThe solution is to make sure the HTTP request doesn't do a separate DNS resolution from the checked one. Depends on which programming language, built-in utilities, 3rd party libraries, etc that you're using.
In the case of Ruby, there is the ssrf_filter gem, which handles all of this for you.