How to handle CNAMEs when resolving domains? - node.js

My web application receives from untrusted user some unfiltered string, and then has to determine, if this string, when used as hostname, somehow resolves to IPv4 or IPv6 address in forbidden range, determined by set of predefined rules.
So, in case string appears to be IPv4 or IPv6 address (either canonical or not), it's simple — just translate address to whatever is it's canonical form, and test if it's in allowed ranges or not.
But what if string is valid hostname, that resolves to lot of records? Using node.js' builtin dns module, I get list of all DNS records for this particular hostname (A, AAAA, TXT, MX, SRV, CNAME). What next? AFAIK, TXT, SRV and MX do not affect name resolution at all. A and AAAA can be verified against aforementioned ruleset.
But what should I do with CNAME? Should I issue recursive DNS resolution for each CNAME encountered? Just ignore it and silently reject? If I issue recursive DNS resolution, any chance to prevent some smarthat feeding my application infinite CNAME stream, like CNAME 1.foobar.com ⟶ CNAME 2.foobar.com ⟶ CNAME 3.foobar.com ⟶ CNAME 4.foobar.com ⟶ ...? In case it repeats at some point, I can break out of it, but what if it does not? If I break early (after N redirections, say), hacker could forge such chain to be N+1 long, with last redirection having A/AAAA records to restricted area.
So, are there solutions to this? How do "convenient" resolvers handle this?

So, I've ended setting up name server myself, and feeding it zone config similar to
$ORIGIN foobar.com
...
evil1 CNAME evil2.foobar.com
evil2 CNAME evil3.foobar.com
evil3 CNAME evil4.foobar.com
evil4 CNAME evil5.foobar.com
...
evil99997 CNAME evil99998.foobar.com
evil99998 CNAME evil99999.foobar.com
evil99999 CNAME evil100000.foobar.com
evil100000 A 127.12.34.56
nslookup request ends as follows:
$ nslookup evil1.foobar.com
Server: 127.0.0.1
Address: 127.0.0.1#53
evil1.foobar.com canonical name = evil2.foobar.com.
evil2.foobar.com canonical name = evil3.foobar.com.
evil3.foobar.com canonical name = evil4.foobar.com.
evil4.foobar.com canonical name = evil5.foobar.com.
evil5.foobar.com canonical name = evil6.foobar.com.
evil6.foobar.com canonical name = evil7.foobar.com.
evil7.foobar.com canonical name = evil8.foobar.com.
evil8.foobar.com canonical name = evil9.foobar.com.
evil9.foobar.com canonical name = evil10.foobar.com.
evil10.foobar.com canonical name = evil11.foobar.com.
evil11.foobar.com canonical name = evil12.foobar.com.
evil12.foobar.com canonical name = evil13.foobar.com.
evil13.foobar.com canonical name = evil14.foobar.com.
evil14.foobar.com canonical name = evil15.foobar.com.
evil15.foobar.com canonical name = evil16.foobar.com.
evil16.foobar.com canonical name = evil17.foobar.com.
evil17.foobar.com canonical name = evil18.foobar.com.
dig produces similar output:
# dig +recurse evil1.foobar.com
; <<>> DiG 9.8.2rc1-RedHat-9.8.2-0.23.rc1.el6_5.1 <<>> +recurse evil1.foobar.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 34317
;; flags: qr aa rd ra; QUERY: 1, ANSWER: 17, AUTHORITY: 0, ADDITIONAL: 0
;; QUESTION SECTION:
;evil1.foobar.com. IN A
;; ANSWER SECTION:
evil1.foobar.com. 10 IN CNAME evil2.foobar.com.
evil2.foobar.com. 10 IN CNAME evil3.foobar.com.
evil3.foobar.com. 10 IN CNAME evil4.foobar.com.
evil4.foobar.com. 10 IN CNAME evil5.foobar.com.
evil5.foobar.com. 10 IN CNAME evil6.foobar.com.
evil6.foobar.com. 10 IN CNAME evil7.foobar.com.
evil7.foobar.com. 10 IN CNAME evil8.foobar.com.
evil8.foobar.com. 10 IN CNAME evil9.foobar.com.
evil9.foobar.com. 10 IN CNAME evil10.foobar.com.
evil10.foobar.com. 10 IN CNAME evil11.foobar.com.
evil11.foobar.com. 10 IN CNAME evil12.foobar.com.
evil12.foobar.com. 10 IN CNAME evil13.foobar.com.
evil13.foobar.com. 10 IN CNAME evil14.foobar.com.
evil14.foobar.com. 10 IN CNAME evil15.foobar.com.
evil15.foobar.com. 10 IN CNAME evil16.foobar.com.
evil16.foobar.com. 10 IN CNAME evil17.foobar.com.
evil17.foobar.com. 10 IN CNAME evil18.foobar.com.
;; Query time: 2 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: ...
;; MSG SIZE rcvd: 388
And according to tests made with plain resolvers, if CNAME chain does not end up with useful target after 16 hops (e.g. if 17th is still CNAME), lookup will be interrupted and domain name will be rejected as non-resolving. CNAME attack myth busted.

I wouldn't mess with any of this, and leave it up to the system resolver.
var dns = require('dns');
dns.lookup('host.example.com');

Related

How can I catch a SERVFAIL exception using Python's dns resolver?

I'm looking to query a domain like this:
dns.resolver.resolve("dnssec-failed.org","A")
Which returns an error like this:
raise NoNameservers(request=self.request, errors=self.errors)
dns.resolver.NoNameservers: All nameservers failed to answer the query dnssec-failed.org. IN A: Server 127.0.0.1 UDP port 53 answered SERVFAIL
I want to be able to catch that exception in my function like so:
def get_a_record(url):
try:
answers = dns.resolver.resolve(url,"A")
except dns.resolver.SERVFAIL:
print("SERVFAIL error for %s" % url)
except dns.resolver.NXDOMAIN:
print("No such domain %s" % url)
except dns.resolver.Timeout:
print("Timed out while resolving %s" % url)
except dns.exception.DNSException:
print("Unhandled exception")
Now I know in the above snippet dns resolver doesn't have a SERVAIL exception but what I'd like to do is catch the error, be able to log it, and continue my script. Is there a proper way to do this using the dns resolver package, or would I need to call the dig command and parse that result?
EDIT
For clarification, I only used dnssec-failed.org as an example because it results in (what I thought) would be the same response as something I am specifically looking, for but don't actually have any active examples of. That "something" being domains which point to ip addresses that are no longer in use. Dangling NS records in other words.
For example I use an IP address that is loaned to me by AWS for use in some XYZ cloud-based application, and I create the name-to-address mapping records in my DNS zone. If I decide to deprecate this service and return the ip back to the cloud provider's pool of ips but forget to remove the DNS record from the zone, it is left "dangling".
That is what I am looking for and I mistakenly assumed that a SERVFAIL is the type of response I get from a query like dig domain-with-no-ip.com
Apologies for the confusion.
EDIT 2
I went and tested this by taking a domain I'd already registered. Configured an A record for it and pointed it to an Ubuntu EC2 listening on port 7272 (python3 -m http.server 7272). Waited 5 minutes for the zone to propagate and then I was able to reach my domain, publicly. All fine and good.
Then I stopped the instance, waited a bit, and then restarted it. Upon coming back up it had a new public ip. Great. So at this point there is a dangling A record for me to test.
So I do dig and nslookup on the domain. Both come back with perfectly fine answers. They just simply point to the now old/original public ip. And that makes sense since the DNS record hasn't changed. The only observable thing that really changes is something like curl, which times out.
So unless my understanding is still wrong, there really isn't an all-too reliable way to hunt down dangling A records because basing logic off n http timeout doesn't necessarily imply a dangling record. The server could just be off/down and the ip is still attached to the resource. Am I correct in my understanding or am I missing something still?
EDIT 3
Accepting the answer because even though my question mildly evolved into something else, the answer did technically address the original question of my post and I think that warrants accepting it.
First, dnssec-failed.org has nameservers but is, by design, failing DNSSEC.
Hence a simple query towards any recursive nameserver that does DNSSEC validation will fail with SERVFAIL as expected:
$ dig dnssec-failed.org NS +short
(no output)
$ dig dnssec-failed.org NS | grep status: | tail -1
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 46517
but as soon as you disable DNSSEC validation you get the nameservers as it should be:
$ dig dnssec-failed.org NS +cdflag +noall +ans +nottlunits
dnssec-failed.org. 7200 IN NS dns105.comcast.net.
dnssec-failed.org. 7200 IN NS dns101.comcast.net.
dnssec-failed.org. 7200 IN NS dns102.comcast.net.
dnssec-failed.org. 7200 IN NS dns103.comcast.net.
dnssec-failed.org. 7200 IN NS dns104.comcast.net.
Now back to the Python part.
resolve() from dnspython is an high level API call, it does everything a resolver does, that is it potentially recurse from root up to being able to give you an answer. Hence, this simple call hides possibly multiple questions and responses and as such may not expose you to the real underlying problem, but provides high level API in output also, using an exception.
As you can see in your own example, you have the SERVFAIL right in the error message, but it is an NoNameservers exceptions because the code asked the registry nameservers for the list of nameservers (which works, there is a DS for this name in parent nameservers), and then ask for any of those nameservers for further data and then they fail there DNSSEC validation, hence the final exception.
It is not clear to me what is your position on DNSSEC error in your case, if you do not care about them or if you really want to study them and do something particular. Hence the above solutions may need to be adapted. If you do not care, just log the NoNameservers exception and go on, everything will work as excepted, DNSSEC validation error will happen exactly like a broken domain, which is per design.
Hence do you really need to handle DNSSEC errors in any way different from any other errors? Why can't you catch NoNameservers exception, log it, and go further?
Otherwise the quick (and dirty way), just parse the error message attached to the NoNameservers exception, and if you see SERVFAIL you can suppose (but not be 100% sure) it is a DNSSEC problem, and at least go further as you need.
If you really need to have further details and be sure it is a DNSSEC problem, you need to do the equivalent of what is above for dig, that is do 2 queries that just differ in the CD DNS flag, and compare results. Which means going "lower" than resolve() API and use dns.query directly, such as this way:
>>> import dns, dns.rcode
>>> resolver_ip = '8.8.8.8' # Use any recursive **validating** nameserver that you trust
>>> query=dns.message.make_query('dnssec-failed.org', 'A')
>>> response = dns.query.udp_with_fallback(query, resolver_ip)[0]
>>> response.rcode() == dns.rcode.SERVFAIL
True
# Now checking if disabling DNSSEC resolves the problem and gets us a reply
# If so, it really means there is a DNSSEC problem
>>> print(str(query))
id 65008
opcode QUERY
rcode NOERROR
flags RD
;QUESTION
dnssec-failed.org. IN A
;ANSWER
;AUTHORITY
;ADDITIONAL
>>> query.flags
<Flag.RD: 256>
>>> query.flags = query.flags | dns.flags.CD
>>> query.flags
<Flag.RD|CD: 272>
>>> print(str(query))
id 65008
opcode QUERY
rcode NOERROR
flags RD CD
;QUESTION
dnssec-failed.org. IN A
;ANSWER
;AUTHORITY
;ADDITIONAL
# We enabled flag "CD" aka checking disabled aka please do not do any DNSSEC validation, and now doing the same query as above again:
>>> response = dns.query.udp_with_fallback(query, resolver_ip)[0]
>>> response.rcode() == dns.rcode.SERVFAIL
False
>>> response.rcode() == dns.rcode.NOERROR
True
>>> response.answer[0][0]
<DNS IN A rdata: 69.252.80.75>

Can we add a dot separated A record in bind?

Is this a correct way to add a Host resource A record in Bind
zone example.com
$TTL 900
# IN SOA ns1.example.com. hostmaster.example.com. (
2017102300 ; serial number
3600 ; refresh
600 ; retry
86400 ; expire
3600 ) ; minimum TTL
# NS ns1.example.com.
foo.bar IN A 192.168.0.1
I know this works well and resolves in public DNS however however question is since bar.example.com becomes a subdomain so foo's host record should be part of bar.example.com zone
I'm no expert, but this looks like a pretty straightforward configuration to me. Even there is no record for bar.example.com everything is okay. Unless if by "part of bar.example.com zone" you mean there is a zone file for bar.example.com, in this case this record will not work, but it does not make it wrongly created.
Source: Years of practice :)

Understanding DNS response header information

I'm in the middle of learning about DNS, and I'm trying to understand how a non-recursive resolver/server would respond to an empty response.
My understanding of DNS is basically that:
If the server returns a non-authoritative response, it will usually provide a list of nameservers (the NSCOUNT) which you can consult to find the authoritative response.
But, what happens if a DNS server returns nothing? As in - just the response header with ANCOUNT = 0, NSCOUNT = 0 and ARCOUNT = 0?
For example, if I query Google's free DNS server (8.8.8.8), and I ask it to resolve "google.com", and the recursion bit is NOT set, this is the response I get:
+---------------------------------------------------------------------------+
| 25550 | QR: 1 | OP: 00 | AA: 0 | TC: 0 | RD: 0 | RA: 1 | Z: 0 | RCODE: 00 |
+---------------------------------------------------------------------------+
| QDCOUNT: 1, ANCOUNT: 0, NSCOUNT: 0, ARCOUNT: 0 |
+---------------------------------------------------------------------------+
So basically, it returned nothing to me except my original query, and it informed me that recursion is available.
In this case, how should the query proceed (assuming we don't just use ask the server to use recursion). Is the only recourse here to contact one of the top-level servers? Or, to put my question another way, how come Google's DNS server didn't return me a list of nameservers (why is NSCOUNT 0?) that I can consult?
When you said "No Recurse", then the Google's NS did not recurse. Since they are not the authoritative nameservers for google.com, they didn't provide any response. This is normal, and acceptable behaviour.
You can only request with "recurse" bit set, to figure out the A-record for google.com. Other way is:
Find the NS for com., from the root servers.
Find the NS for google.com from one of the com NS.
Find the A record for google.com from the gooogle.com NS.
Basically, you do what the recursive nameserver was supposed to do for you.
Note: Recursive NS can use its cache for getting you a response without actual queries, based on TTL for the record (and of course if you set the recursion bit (-:
Only an authoritative server is supposed to include the NS records in the authority section of the response.
The Google 8.8.8.8 servers are not authoritative for google.com, and you asked them not to recurse, so they didn't.
This is an abnormal query that a real DNS client wouldn't send to them, so their response of "NO DATA / NO ERROR" (RCODE == 0, ANCOUNT == 0) is acceptable.

dig "hostname_1" #"IP" command

I was told to execute the command: dig "hostname_1" #"IP".
I don't know what it is for, any idea? and the meaning of "#IP"?
Another question, the response has the field:
;;AUTHORITY SECTION:
"hostname_1" 1200 IN NS "hostname_2"
"hostname_1" 1200 IN NS "hostname_3"
Is it correct that hostname_2 and hostname_3 are another names for hostname_1?or are they nameservers of the hostname_1 host?
dig is a tool for performing DNS lookups.
Normally dig asks your locally configured nameserver, however, with #IP you can make dig ask the nameserver which runs on the specified IP.
The output of dig can be read a follows KEY, TTL (time to live in seconds), CLASS (normally "IN" for Internet), TYPE, RDATA (resource data) (see https://en.wikipedia.org/wiki/Resource_record for a longer description)
There are a number of types (see https://en.wikipedia.org/wiki/List_of_DNS_record_types), NS means "nameserver". In your case hostname_2 and hostname_3 are the responsible nameservers for hostname_1.

Amazon Route 53 DNS Reverse Lookup Zone - ATT IP Block Setup

I cannot get reverse DNS lookup configured correctly in Amazon Route 53 for a block of ATT IP's that have been delegated to Amazon Name Servers.
So, I have a block of IP's assigned to me by ATT. The block is 12.237.40.64/27. ATT has delegate the block to Amazon Name Servers. I start by doing a dig -x on one of the IP's in the block - .65, and get the following:
;; ANSWER SECTION:
65.40.237.12.in-addr.arpa. 86400 IN CNAME 65.64/27.40.237.12.in-addr.arpa.
So I start to create a zone in Amazon Route 53 with the name 65.64/27.40.237.12.in-addr.arpa < this value is being entered into the 'Domain Name' field with in Amazon Route 53 in the UI. Once created and saved, Route 53 actually names the zone:
65.64\05727.40.237.12.in-addr.arpa
Not sure why this is happening. It seems that the CNAME entry from ATT about will never find the zone 65.64/27.40.237.12.in-addr.arpa ?? So I continue and assume that some sort of translation will occur from the CNAME entry to the zone name I view on screen within route 53. Once the zone is created. I proceed to try to add a PTR record in Route 53. Route 53 will only allow me to prepend the zone name with an ip so the result is:
65.65.64\05727.40.237.12.in-addr.arpa PTR server.mydomain.com
The result I want would be:
65.40.237.12.in-addr.arpa. PTR server.mydomain.com.
So I add a new zone file and name the zone 40.237.12.in-addr.arpa. I then add the PTR record for .65 with in the block. I then dig -x 12.237.40.65 #amz.name.server, and I get the desired result:
;; ANSWER SECTION:
65.40.237.12.in-addr.arpa. 300 IN PTR server.mydomain.com.
So the problem seems to be that I cannot name the Route 53 zone to agree with the CNAME delegation from ATT ??
I found an article (PDF) on ATT delegated IP blocks.
Can anyone give me some direction on setting up reverse DNS on Amazon route 53 correctly? Or do I need to contact ATT to change the CNAME entry to a different format that I can add and save in route 53?
65.64/27.40.237.12.in-addr.arpa. and 65.65.64\05727.40.237.12.in-addr.arpa PTR server.mydomain.com are just slightly differently formatted names for the same thing. \057 is the octal code for /. It looks like you've configured the domain correctly ...
colmmacc#colmmac] dig NS 64/27.40.237.12.in-addr.arpa. #ns-1175.awsdns-18.org
<snip>
;; QUESTION SECTION:
;64/27.40.237.12.in-addr.arpa. IN NS
;; ANSWER SECTION:
64/27.40.237.12.in-addr.arpa. 3600 IN NS ns-1175.awsdns-18.org.
64/27.40.237.12.in-addr.arpa. 3600 IN NS ns-1967.awsdns-53.co.uk.
64/27.40.237.12.in-addr.arpa. 3600 IN NS ns-281.awsdns-35.com.
64/27.40.237.12.in-addr.arpa. 3600 IN NS ns-593.awsdns-10.net.
and the reverse DNS entry seems to be working too:
colmmacc#colmmacc] dig -x 12.237.40.65
;; QUESTION SECTION:
;65.40.237.12.in-addr.arpa. IN PTR
;; ANSWER SECTION:
65.40.237.12.in-addr.arpa. 86400 IN CNAME 65.64/27.40.237.12.in-addr.arpa.
65.64/27.40.237.12.in-addr.arpa. 300 IN PTR smtpwin.omnilink.com.
Route 53 merely escapes all non-alpha-numerical characters for display, but the / and \057
are equivalent.

Resources