From 40d0f115a64595aa83cfe0b760587d3d1efa0385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tatuya=20JINMEI=20=E7=A5=9E=E6=98=8E=E9=81=94=E5=93=89?= Date: Fri, 29 May 2009 22:22:37 +0000 Subject: [PATCH] 2604. [func] Add support for DNS rebinding attack prevention through new options, deny-answer-addresses and deny-answer-aliases. Based on contributed code from JD Nurmi, Google. [RT #18192] --- CHANGES | 5 + README | 2 + bin/named/bind.keys.h | 22 +-- bin/named/server.c | 146 +++++++++++++++++--- bin/tests/system/resolver/ans2/ans.pl | 29 +++- bin/tests/system/resolver/ans3/ans.pl | 39 +++++- bin/tests/system/resolver/clean.sh | 3 +- bin/tests/system/resolver/ns1/named.conf | 7 +- bin/tests/system/resolver/tests.sh | 72 +++++++++- doc/arm/Bv9ARM-book.xml | 153 ++++++++++++++++++++- lib/dns/include/dns/view.h | 6 +- lib/dns/resolver.c | 163 ++++++++++++++++++++++- lib/dns/view.c | 15 ++- lib/isccfg/namedconf.c | 32 ++++- 14 files changed, 641 insertions(+), 53 deletions(-) diff --git a/CHANGES b/CHANGES index 575b1baf40..e52e42c52a 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,8 @@ +2604. [func] Add support for DNS rebinding attack prevention through + new options, deny-answer-addresses and + deny-answer-aliases. Based on contributed code from + JD Nurmi, Google. [RT #18192] + 2603. [port] win32: handle .exe extension of named-checkzone and named-comilezone argv[0] names under windows. [RT #19767] diff --git a/README b/README index 3b89403b64..8abc9d4065 100644 --- a/README +++ b/README @@ -54,6 +54,8 @@ BIND 9.7.0 internal information about query failures, especially about server failures. + Add support for DNS rebinding attack prevention. + BIND 9.6.0 BIND 9.6.0 includes a number of changes from BIND 9.5 and earlier diff --git a/bin/named/bind.keys.h b/bin/named/bind.keys.h index 3486fc1fd9..1b287a5184 100644 --- a/bin/named/bind.keys.h +++ b/bin/named/bind.keys.h @@ -1,25 +1,7 @@ -/* - * Copyright (C) 2009 Internet Systems Consortium, Inc. ("ISC") - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH - * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, - * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE - * OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - * PERFORMANCE OF THIS SOFTWARE. - */ - -/* $Id: bind.keys.h,v 1.3 2009/03/05 23:47:35 tbox Exp $ */ - #define TRUSTED_KEYS "\ trusted-keys {\n\ - # NOTE: This key expires September 2009 \n\ - # Go to https://www.isc.org/solutions/dlv to download a replacement\n\ + # NOTE: This key expires September 2009 \n\ + # Go to https://www.isc.org/solutions/dlv to download a replacement\n\ dlv.isc.org. 257 3 5 \"BEAAAAPHMu/5onzrEE7z1egmhg/WPO0+juoZrW3euWEn4MxDCE1+lLy2 brhQv5rN32RKtMzX6Mj70jdzeND4XknW58dnJNPCxn8+jAGl2FZLK8t+ 1uq4W+nnA3qO2+DL+k6BD4mewMLbIYFwe0PG73Te9fZ2kJb56dhgMde5 ymX4BI/oQ+cAK50/xvJv00Frf8kw6ucMTwFlgPe+jnGxPPEmHAte/URk Y62ZfkLoBAADLHQ9IrS2tryAe7mbBZVcOwIeU/Rw/mRx/vwwMCTgNboM QKtUdvNXDrYJDSHZws3xiRXF1Rf+al9UmZfSav/4NWLKjHzpT59k/VSt TDN0YUuWrBNh\";\n\ };\n\ " diff --git a/bin/named/server.c b/bin/named/server.c index 04a25361c1..0e25f9beb7 100644 --- a/bin/named/server.c +++ b/bin/named/server.c @@ -15,7 +15,7 @@ * PERFORMANCE OF THIS SOFTWARE. */ -/* $Id: server.c,v 1.530 2009/03/04 23:48:01 tbox Exp $ */ +/* $Id: server.c,v 1.531 2009/05/29 22:22:35 jinmei Exp $ */ /*! \file */ @@ -278,8 +278,8 @@ end_reserved_dispatches(ns_server_t *server, isc_boolean_t all); */ static isc_result_t configure_view_acl(const cfg_obj_t *vconfig, const cfg_obj_t *config, - const char *aclname, cfg_aclconfctx_t *actx, - isc_mem_t *mctx, dns_acl_t **aclp) + const char *aclname, const char *acltuplename, + cfg_aclconfctx_t *actx, isc_mem_t *mctx, dns_acl_t **aclp) { isc_result_t result; const cfg_obj_t *maps[3]; @@ -305,13 +305,21 @@ configure_view_acl(const cfg_obj_t *vconfig, const cfg_obj_t *config, */ return (ISC_R_SUCCESS); + if (acltuplename != NULL) { + /* + * If the ACL is given in an optional tuple, retrieve it. + * The parser should have ensured that a valid object be + * returned. + */ + aclobj = cfg_tuple_get(aclobj, acltuplename); + } + result = cfg_acl_fromconfig(aclobj, config, ns_g_lctx, actx, mctx, 0, aclp); return (result); } - /*% * Configure a sortlist at '*aclp'. Essentially the same as * configure_view_acl() except it calls cfg_acl_fromconfig with a @@ -355,6 +363,80 @@ configure_view_sortlist(const cfg_obj_t *vconfig, const cfg_obj_t *config, return (result); } +static isc_result_t +configure_view_nametable(const cfg_obj_t *vconfig, const cfg_obj_t *config, + const char *confname, const char *conftuplename, + isc_mem_t *mctx, dns_rbt_t **rbtp) +{ + isc_result_t result; + const cfg_obj_t *maps[3]; + const cfg_obj_t *obj = NULL; + const cfg_listelt_t *element; + int i = 0; + dns_fixedname_t fixed; + dns_name_t *name; + isc_buffer_t b; + const char *str; + const cfg_obj_t *nameobj; + + if (*rbtp != NULL) + dns_rbt_destroy(rbtp); + if (vconfig != NULL) + maps[i++] = cfg_tuple_get(vconfig, "options"); + if (config != NULL) { + const cfg_obj_t *options = NULL; + (void)cfg_map_get(config, "options", &options); + if (options != NULL) + maps[i++] = options; + } + maps[i] = NULL; + + (void)ns_config_get(maps, confname, &obj); + if (obj == NULL) + /* + * No value available. *rbtp == NULL. + */ + return (ISC_R_SUCCESS); + + if (conftuplename != NULL) { + obj = cfg_tuple_get(obj, conftuplename); + if (cfg_obj_isvoid(obj)) + return (ISC_R_SUCCESS); + } + + result = dns_rbt_create(mctx, NULL, NULL, rbtp); + if (result != ISC_R_SUCCESS) + return (result); + + dns_fixedname_init(&fixed); + name = dns_fixedname_name(&fixed); + for (element = cfg_list_first(obj); + element != NULL; + element = cfg_list_next(element)) { + nameobj = cfg_listelt_value(element); + str = cfg_obj_asstring(nameobj); + isc_buffer_init(&b, str, strlen(str)); + isc_buffer_add(&b, strlen(str)); + CHECK(dns_name_fromtext(name, &b, dns_rootname, + ISC_FALSE, NULL)); + /* + * We don't need the node data, but need to set dummy data to + * avoid a partial match with an empty node. For example, if + * we have foo.example.com and bar.example.com, we'd get a match + * for baz.example.com, which is not the expected result. + * We simply use (void *)1 as the dummy data. + */ + CHECK(dns_rbt_addname(*rbtp, name, (void *)1)); + } + + return (result); + + cleanup: + dns_rbt_destroy(rbtp); + return (result); + +} + static isc_result_t configure_view_dnsseckey(const cfg_obj_t *vconfig, const cfg_obj_t *key, dns_keytable_t *keytable, isc_mem_t *mctx) @@ -1722,10 +1804,10 @@ configure_view(dns_view_t *view, const cfg_obj_t *config, /* * Configure the "match-clients" and "match-destinations" ACL. */ - CHECK(configure_view_acl(vconfig, config, "match-clients", actx, + CHECK(configure_view_acl(vconfig, config, "match-clients", NULL, actx, ns_g_mctx, &view->matchclients)); - CHECK(configure_view_acl(vconfig, config, "match-destinations", actx, - ns_g_mctx, &view->matchdestinations)); + CHECK(configure_view_acl(vconfig, config, "match-destinations", NULL, + actx, ns_g_mctx, &view->matchdestinations)); /* * Configure the "match-recursive-only" option. @@ -1797,20 +1879,20 @@ configure_view(dns_view_t *view, const cfg_obj_t *config, * "allow-recursion", and "allow-recursion-on" acls if * configured in named.conf. */ - CHECK(configure_view_acl(vconfig, config, "allow-query-cache", + CHECK(configure_view_acl(vconfig, config, "allow-query-cache", NULL, actx, ns_g_mctx, &view->queryacl)); - CHECK(configure_view_acl(vconfig, config, "allow-query-cache-on", + CHECK(configure_view_acl(vconfig, config, "allow-query-cache-on", NULL, actx, ns_g_mctx, &view->queryonacl)); if (view->queryonacl == NULL) CHECK(configure_view_acl(NULL, ns_g_config, - "allow-query-cache-on", actx, + "allow-query-cache-on", NULL, actx, ns_g_mctx, &view->queryonacl)); if (strcmp(view->name, "_bind") != 0) { CHECK(configure_view_acl(vconfig, config, "allow-recursion", - actx, ns_g_mctx, + NULL, actx, ns_g_mctx, &view->recursionacl)); CHECK(configure_view_acl(vconfig, config, "allow-recursion-on", - actx, ns_g_mctx, + NULL, actx, ns_g_mctx, &view->recursiononacl)); } @@ -1823,7 +1905,7 @@ configure_view(dns_view_t *view, const cfg_obj_t *config, if (view->queryacl == NULL && view->recursionacl != NULL) dns_acl_attach(view->recursionacl, &view->queryacl); if (view->queryacl == NULL && view->recursion) - CHECK(configure_view_acl(vconfig, config, "allow-query", + CHECK(configure_view_acl(vconfig, config, "allow-query", NULL, actx, ns_g_mctx, &view->queryacl)); if (view->recursion && view->recursionacl == NULL && view->queryacl != NULL) @@ -1835,19 +1917,20 @@ configure_view(dns_view_t *view, const cfg_obj_t *config, */ if (view->recursionacl == NULL && view->recursion) CHECK(configure_view_acl(NULL, ns_g_config, - "allow-recursion", + "allow-recursion", NULL, actx, ns_g_mctx, &view->recursionacl)); if (view->recursiononacl == NULL && view->recursion) CHECK(configure_view_acl(NULL, ns_g_config, - "allow-recursion-on", + "allow-recursion-on", NULL, actx, ns_g_mctx, &view->recursiononacl)); if (view->queryacl == NULL) { if (view->recursion) CHECK(configure_view_acl(NULL, ns_g_config, - "allow-query-cache", actx, - ns_g_mctx, &view->queryacl)); + "allow-query-cache", NULL, + actx, ns_g_mctx, + &view->queryacl)); else { if (view->queryacl != NULL) dns_acl_detach(&view->queryacl); @@ -1855,6 +1938,25 @@ configure_view(dns_view_t *view, const cfg_obj_t *config, } } + /* + * Filter setting on addresses in the answer section. + */ + CHECK(configure_view_acl(vconfig, config, "deny-answer-addresses", + "acl", actx, ns_g_mctx, &view->denyansweracl)); + CHECK(configure_view_nametable(vconfig, config, "deny-answer-addresses", + "except-from", ns_g_mctx, + &view->answeracl_exclude)); + + /* + * Filter setting on names (CNAME/DNAME targets) in the answer section. + */ + CHECK(configure_view_nametable(vconfig, config, "deny-answer-aliases", + "name", ns_g_mctx, + &view->denyanswernames)); + CHECK(configure_view_nametable(vconfig, config, "deny-answer-aliases", + "except-from", ns_g_mctx, + &view->answernames_exclude)); + /* * Configure sortlist, if set */ @@ -1868,19 +1970,19 @@ configure_view(dns_view_t *view, const cfg_obj_t *config, */ if (view->notifyacl == NULL) CHECK(configure_view_acl(NULL, ns_g_config, - "allow-notify", actx, + "allow-notify", NULL, actx, ns_g_mctx, &view->notifyacl)); if (view->transferacl == NULL) CHECK(configure_view_acl(NULL, ns_g_config, - "allow-transfer", actx, + "allow-transfer", NULL, actx, ns_g_mctx, &view->transferacl)); if (view->updateacl == NULL) CHECK(configure_view_acl(NULL, ns_g_config, - "allow-update", actx, + "allow-update", NULL, actx, ns_g_mctx, &view->updateacl)); if (view->upfwdacl == NULL) CHECK(configure_view_acl(NULL, ns_g_config, - "allow-update-forwarding", actx, + "allow-update-forwarding", NULL, actx, ns_g_mctx, &view->upfwdacl)); obj = NULL; @@ -3301,7 +3403,7 @@ load_configuration(const char *filename, ns_server_t *server, else isc_quota_soft(&server->recursionquota, 0); - CHECK(configure_view_acl(NULL, config, "blackhole", &aclconfctx, + CHECK(configure_view_acl(NULL, config, "blackhole", NULL, &aclconfctx, ns_g_mctx, &server->blackholeacl)); if (server->blackholeacl != NULL) dns_dispatchmgr_setblackhole(ns_g_dispatchmgr, diff --git a/bin/tests/system/resolver/ans2/ans.pl b/bin/tests/system/resolver/ans2/ans.pl index b41f1986b3..acfe4e1a35 100644 --- a/bin/tests/system/resolver/ans2/ans.pl +++ b/bin/tests/system/resolver/ans2/ans.pl @@ -15,7 +15,7 @@ # OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # PERFORMANCE OF THIS SOFTWARE. -# $Id: ans.pl,v 1.10 2007/09/24 04:13:25 marka Exp $ +# $Id: ans.pl,v 1.11 2009/05/29 22:22:36 jinmei Exp $ # # Ad hoc name server @@ -52,6 +52,7 @@ for (;;) { my @questions = $packet->question; my $qname = $questions[0]->qname; + my $qtype = $questions[0]->qtype; if ($qname eq "cname1.example.com") { # Data for the "cname + other data / 1" test @@ -61,6 +62,32 @@ for (;;) { # Data for the "cname + other data / 2" test: same RRs in opposite order $packet->push("answer", new Net::DNS::RR("cname2.example.com 300 A 1.2.3.4")); $packet->push("answer", new Net::DNS::RR("cname2.example.com 300 CNAME cname2.example.com")); + } elsif ($qname eq "www.example.org" || $qname eq "www.example.net" || + $qname eq "badcname.example.org" || + $qname eq "goodcname.example.org" || + $qname eq "foo.baddname.example.org" || + $qname eq "foo.gooddname.example.org") { + # Data for address/alias filtering. + if ($qtype eq "A") { + $packet->push("answer", + new Net::DNS::RR($qname . + " 300 A 192.0.2.1")); + } elsif ($qtype eq "AAAA") { + $packet->push("answer", + new Net::DNS::RR($qname . + " 300 AAAA 2001:db8:beef::1")); + } + } elsif ($qname eq "badcname.example.net" || + $qname eq "goodcname.example.net") { + # Data for CNAME/DNAME filtering. We need to make one-level + # delegation to avoid automatic acceptance for subdomain aliases + $packet->push("authority", new Net::DNS::RR("example.net 300 NS ns.example.net")); + $packet->push("additional", new Net::DNS::RR("ns.example.net 300 A 10.53.0.3")); + } elsif ($qname =~ /sub\.example\.org/) { + # Data for CNAME/DNAME filtering. The final answers are + # expected to be accepted regardless of the filter setting. + $packet->push("authority", new Net::DNS::RR("sub.example.org 300 NS ns.sub.example.org")); + $packet->push("additional", new Net::DNS::RR("ns.sub.example.org 300 A 10.53.0.3")); } else { # Data for the "bogus referrals" test $packet->push("authority", new Net::DNS::RR("below.www.example.com 300 NS ns.below.www.example.com")); diff --git a/bin/tests/system/resolver/ans3/ans.pl b/bin/tests/system/resolver/ans3/ans.pl index 3053b25fe1..3cc7f6858b 100644 --- a/bin/tests/system/resolver/ans3/ans.pl +++ b/bin/tests/system/resolver/ans3/ans.pl @@ -15,7 +15,7 @@ # OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # PERFORMANCE OF THIS SOFTWARE. -# $Id: ans.pl,v 1.9 2007/09/24 04:13:25 marka Exp $ +# $Id: ans.pl,v 1.10 2009/05/29 22:22:36 jinmei Exp $ # # Ad hoc name server @@ -50,7 +50,42 @@ for (;;) { $packet->header->qr(1); - $packet->push("answer", new Net::DNS::RR("www.example.com 300 A 1.2.3.4")); + my @questions = $packet->question; + my $qname = $questions[0]->qname; + + if ($qname eq "badcname.example.net") { + $packet->push("answer", + new Net::DNS::RR($qname . + " 300 CNAME badcname.example.org")); + } elsif ($qname eq "foo.baddname.example.net") { + $packet->push("answer", + new Net::DNS::RR("baddname.example.net" . + " 300 DNAME baddname.example.org")); + } elsif ($qname eq "foo.gooddname.example.net") { + $packet->push("answer", + new Net::DNS::RR("gooddname.example.net" . + " 300 DNAME gooddname.example.org")); + } elsif ($qname eq "goodcname.example.net") { + $packet->push("answer", + new Net::DNS::RR($qname . + " 300 CNAME goodcname.example.org")); + } elsif ($qname eq "cname.sub.example.org") { + $packet->push("answer", + new Net::DNS::RR($qname . + " 300 CNAME ok.sub.example.org")); + } elsif ($qname eq "ok.sub.example.org") { + $packet->push("answer", + new Net::DNS::RR($qname . " 300 A 192.0.2.1")); + } elsif ($qname eq "www.dname.sub.example.org") { + $packet->push("answer", + new Net::DNS::RR("dname.sub.example.org" . + " 300 DNAME ok.sub.example.org")); + } elsif ($qname eq "www.ok.sub.example.org") { + $packet->push("answer", + new Net::DNS::RR($qname . " 300 A 192.0.2.1")); + } else { + $packet->push("answer", new Net::DNS::RR("www.example.com 300 A 1.2.3.4")); + } $sock->send($packet->data); diff --git a/bin/tests/system/resolver/clean.sh b/bin/tests/system/resolver/clean.sh index c79da92819..8cc1b3b672 100644 --- a/bin/tests/system/resolver/clean.sh +++ b/bin/tests/system/resolver/clean.sh @@ -14,9 +14,10 @@ # OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # PERFORMANCE OF THIS SOFTWARE. -# $Id: clean.sh,v 1.1 2008/07/17 01:15:34 marka Exp $ +# $Id: clean.sh,v 1.2 2009/05/29 22:22:36 jinmei Exp $ # # Clean up after resolver tests. # rm -f */named.memstats +rm -f dig.out diff --git a/bin/tests/system/resolver/ns1/named.conf b/bin/tests/system/resolver/ns1/named.conf index 4b0c80abf9..8cc21e525f 100644 --- a/bin/tests/system/resolver/ns1/named.conf +++ b/bin/tests/system/resolver/ns1/named.conf @@ -15,7 +15,7 @@ * PERFORMANCE OF THIS SOFTWARE. */ -/* $Id: named.conf,v 1.13 2007/06/18 23:47:30 tbox Exp $ */ +/* $Id: named.conf,v 1.14 2009/05/29 22:22:36 jinmei Exp $ */ controls { /* empty */ }; @@ -29,6 +29,11 @@ options { listen-on-v6 { none; }; recursion yes; acache-enable yes; + deny-answer-addresses { 192.0.2.0/24; 2001:db8:beef::/48; } + except-from { "example.org"; }; + deny-answer-aliases { "example.org"; } + except-from { "goodcname.example.net"; + "gooddname.example.net"; }; }; zone "." { diff --git a/bin/tests/system/resolver/tests.sh b/bin/tests/system/resolver/tests.sh index 585455c9a5..a30e8e4775 100644 --- a/bin/tests/system/resolver/tests.sh +++ b/bin/tests/system/resolver/tests.sh @@ -15,7 +15,7 @@ # OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR # PERFORMANCE OF THIS SOFTWARE. -# $Id: tests.sh,v 1.9 2007/06/19 23:47:05 tbox Exp $ +# $Id: tests.sh,v 1.10 2009/05/29 22:22:36 jinmei Exp $ SYSTEMTESTTOP=.. . $SYSTEMTESTTOP/conf.sh @@ -35,5 +35,75 @@ $DIG +tcp cname2.example.com. a @10.53.0.1 -p 5300 >/dev/null || status=1 echo "I:check that server is still running" $DIG +tcp www.example.com. a @10.53.0.1 -p 5300 >/dev/null || status=1 +echo "I:checking answer IPv4 address filtering (deny)" +ret=0 +$DIG +tcp www.example.net @10.53.0.1 a -p 5300 > dig.out || ret=1 +grep "status: SERVFAIL" dig.out > /dev/null || ret=1 +if [ $ret != 0 ]; then echo "I:failed"; fi +status=`expr $status + $ret` + +echo "I:checking answer IPv6 address filtering (deny)" +ret=0 +$DIG +tcp www.example.net @10.53.0.1 aaaa -p 5300 > dig.out || ret=1 +grep "status: SERVFAIL" dig.out > /dev/null || ret=1 +if [ $ret != 0 ]; then echo "I:failed"; fi +status=`expr $status + $ret` + +echo "I:checking answer IPv4 address filtering (accept)" +ret=0 +$DIG +tcp www.example.org @10.53.0.1 a -p 5300 > dig.out || ret=1 +grep "status: NOERROR" dig.out > /dev/null || ret=1 +if [ $ret != 0 ]; then echo "I:failed"; fi +status=`expr $status + $ret` + +echo "I:checking answer IPv6 address filtering (accept)" +ret=0 +$DIG +tcp www.example.org @10.53.0.1 aaaa -p 5300 > dig.out || ret=1 +grep "status: NOERROR" dig.out > /dev/null || ret=1 +if [ $ret != 0 ]; then echo "I:failed"; fi +status=`expr $status + $ret` + +echo "I:checking CNAME target filtering (deny)" +ret=0 +$DIG +tcp badcname.example.net @10.53.0.1 a -p 5300 > dig.out || ret=1 +grep "status: SERVFAIL" dig.out > /dev/null || ret=1 +if [ $ret != 0 ]; then echo "I:failed"; fi +status=`expr $status + $ret` + +echo "I:checking CNAME target filtering (accept)" +ret=0 +$DIG +tcp goodcname.example.net @10.53.0.1 a -p 5300 > dig.out || ret=1 +grep "status: NOERROR" dig.out > /dev/null || ret=1 +if [ $ret != 0 ]; then echo "I:failed"; fi +status=`expr $status + $ret` + +echo "I:checking CNAME target filtering (accept due to subdomain)" +ret=0 +$DIG +tcp cname.sub.example.org @10.53.0.1 a -p 5300 > dig.out || ret=1 +grep "status: NOERROR" dig.out > /dev/null || ret=1 +if [ $ret != 0 ]; then echo "I:failed"; fi +status=`expr $status + $ret` + +echo "I:checking DNAME target filtering (deny)" +ret=0 +$DIG +tcp foo.baddname.example.net @10.53.0.1 a -p 5300 > dig.out || ret=1 +grep "status: SERVFAIL" dig.out > /dev/null || ret=1 +if [ $ret != 0 ]; then echo "I:failed"; fi +status=`expr $status + $ret` + +echo "I:checking DNAME target filtering (accept)" +ret=0 +$DIG +tcp foo.gooddname.example.net @10.53.0.1 a -p 5300 > dig.out || ret=1 +grep "status: NOERROR" dig.out > /dev/null || ret=1 +if [ $ret != 0 ]; then echo "I:failed"; fi +status=`expr $status + $ret` + +echo "I:checking DNAME target filtering (accept due to subdomain)" +ret=0 +$DIG +tcp www.dname.sub.example.org @10.53.0.1 a -p 5300 > dig.out || ret=1 +grep "status: NOERROR" dig.out > /dev/null || ret=1 +if [ $ret != 0 ]; then echo "I:failed"; fi +status=`expr $status + $ret` + echo "I:exit status: $status" exit $status diff --git a/doc/arm/Bv9ARM-book.xml b/doc/arm/Bv9ARM-book.xml index 32d4c81ebe..56511a6e38 100644 --- a/doc/arm/Bv9ARM-book.xml +++ b/doc/arm/Bv9ARM-book.xml @@ -18,7 +18,7 @@ - PERFORMANCE OF THIS SOFTWARE. --> - + BIND 9 Administrator Reference Manual @@ -2861,6 +2861,19 @@ $ORIGIN 0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa. + + + + namelist + + + + + A list of one or more domain_name + elements. + + + @@ -4951,6 +4964,8 @@ badresp:1,adberr:0,findfail:0,valfail:0] disable-empty-zone zone_name ; zero-no-soa-ttl yes_or_no ; zero-no-soa-ttl-cache yes_or_no ; + deny-answer-addresses { address_match_list } except-from { namelist } ; + deny-answer-aliases { namelist } except-from { namelist } ; }; @@ -8426,6 +8441,142 @@ XXX: end of RFC1918 addresses #defined out --> + + Content Filtering + + BIND 9 provides the ability to filter + out DNS responses from external DNS servers containing + certain types of data in the answer section. + Specifically, it can reject address (A or AAAA) records if + the corresponding IPv4 or IPv6 addresses match the given + address_match_list of the + deny-answer-addresses option. + It can also reject CNAME or DNAME records if the "alias" + name (i.e., the CNAME alias or the substituted query name + due to DNAME) matches the + given namelist of the + deny-answer-aliases option, where + "match" means the alias name is a subdomain of one of + the name_list elements. + If the optional namelist is specified + with except-from, records whose query name + matches the list will be accepted regardless of the filter + setting. + Likewise, if the alias name is a subdomain of the + corresponding zone, the deny-answer-aliases + filter will not apply; + for example, even if "example.com" is specified for + deny-answer-aliases, + +www.example.com. CNAME xxx.example.com. + + + returned by an "example.com" server will be accepted. + + + + In the address_match_list of the + deny-answer-addresses option, only + ip_addr + and ip_prefix + are meaningful; + any key_id will be silently ignored. + + + + If a response message is rejected due to the filtering, + the entire message is discarded without being cached, and + a SERVFAIL error will be returned to the client. + + + + This filtering is intended to prevent "DNS rebinding attacks," in + which an attacker, in response to a query for a domain name the + attacker controls, returns an IP address within your own network or + an alias name within your own domain. + A naive web browser or script could then serve as an + unintended proxy, allowing the attacker + to get access to an internal node of your local network + that couldn't be externally accessed otherwise. + See the paper available at + + http://portal.acm.org/citation.cfm?id=1315245.1315298 + + for more details about the attacks. + + + + For example, if you own a domain named "example.net" and + your internal network uses an IPv4 prefix 192.0.2.0/24, + you might specify the following rules: + + +deny-answer-addresses { 192.0.2.0/24; } except-from { "example.net"; }; +deny-answer-aliases { "example.net"; }; + + + + If an external attacker lets a web browser in your local + network look up an IPv4 address of "attacker.example.com", + the attacker's DNS server would return a response like this: + + +attacker.example.com. A 192.0.2.1 + + + in the answer section. + Since the rdata of this record (the IPv4 address) matches + the specified prefix 192.0.2.0/24, this response will be + ignored. + + + + On the other hand, if the browser looks up a legitimate + internal web server "www.example.net" and the + following response is returned to + the BIND 9 server + + +www.example.net. A 192.0.2.2 + + + it will be accepted since the owner name "www.example.net" + matches the except-from element, + "example.net". + + + + Note that this is not really an attack on the DNS per se. + In fact, there is nothing wrong for an "external" name to + be mapped to your "internal" IP address or domain name + from the DNS point of view. + It might actually be provided for a legitimate purpose, + such as for debugging. + As long as the mapping is provided by the correct owner, + it is not possible or does not make sense to detect + whether the intent of the mapping is legitimate or not + within the DNS. + The "rebinding" attack must primarily be protected at the + application that uses the DNS. + For a large site, however, it may be difficult to protect + all possible applications at once. + This filtering feature is provided only to help such an + operational environment; + it is generally discouraged to turn it on unless you are + very sure you have no other choice and the attack is a + real threat for your applications. + + + + Care should be particularly taken if you want to use this + option for addresses within 127.0.0.0/8. + These addresses are obviously "internal", but many + applications conventionally rely on a DNS mapping from + some name to such an address. + Filtering out DNS records containing this address + spuriously can break such applications. + + diff --git a/lib/dns/include/dns/view.h b/lib/dns/include/dns/view.h index 6c99ef1f7f..d275b10fb6 100644 --- a/lib/dns/include/dns/view.h +++ b/lib/dns/include/dns/view.h @@ -15,7 +15,7 @@ * PERFORMANCE OF THIS SOFTWARE. */ -/* $Id: view.h,v 1.116 2009/01/27 22:29:59 jinmei Exp $ */ +/* $Id: view.h,v 1.117 2009/05/29 22:22:37 jinmei Exp $ */ #ifndef DNS_VIEW_H #define DNS_VIEW_H 1 @@ -128,6 +128,10 @@ struct dns_view { dns_acl_t * transferacl; dns_acl_t * updateacl; dns_acl_t * upfwdacl; + dns_acl_t * denyansweracl; + dns_rbt_t * answeracl_exclude; + dns_rbt_t * denyanswernames; + dns_rbt_t * answernames_exclude; isc_boolean_t requestixfr; isc_boolean_t provideixfr; isc_boolean_t requestnsid; diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c index a06c1eeb63..a1b12d388d 100644 --- a/lib/dns/resolver.c +++ b/lib/dns/resolver.c @@ -15,7 +15,7 @@ * PERFORMANCE OF THIS SOFTWARE. */ -/* $Id: resolver.c,v 1.398 2009/05/11 02:38:35 tbox Exp $ */ +/* $Id: resolver.c,v 1.399 2009/05/29 22:22:36 jinmei Exp $ */ /*! \file */ @@ -4837,6 +4837,134 @@ dname_target(dns_rdataset_t *rdataset, dns_name_t *qname, dns_name_t *oname, return (result); } +static isc_boolean_t +is_answeraddress_allowed(dns_view_t *view, dns_name_t *name, + dns_rdataset_t *rdataset) +{ + isc_result_t result; + dns_rdata_t rdata = DNS_RDATA_INIT; + struct in_addr ina; + struct in6_addr in6a; + isc_netaddr_t netaddr; + char addrbuf[ISC_NETADDR_FORMATSIZE]; + char namebuf[DNS_NAME_FORMATSIZE]; + char classbuf[64]; + char typebuf[64]; + int match; + + /* By default, we allow any addresses. */ + if (view->denyansweracl == NULL) + return (ISC_TRUE); + + /* + * If the owner name matches one in the exclusion list, either exactly + * or partially, allow it. + */ + if (view->answeracl_exclude != NULL) { + dns_rbtnode_t *node = NULL; + + result = dns_rbt_findnode(view->answeracl_exclude, name, NULL, + &node, NULL, 0, NULL, NULL); + + if (result == ISC_R_SUCCESS || result == DNS_R_PARTIALMATCH) + return (ISC_TRUE); + } + + /* + * Otherwise, search the filter list for a match for each address + * record. If a match is found, the address should be filtered, + * so should the entire answer. + */ + for (result = dns_rdataset_first(rdataset); + result == ISC_R_SUCCESS; + result = dns_rdataset_next(rdataset)) { + dns_rdata_reset(&rdata); + dns_rdataset_current(rdataset, &rdata); + if (rdataset->type == dns_rdatatype_a) { + INSIST(rdata.length == sizeof(ina.s_addr)); + memcpy(&ina.s_addr, rdata.data, sizeof(ina.s_addr)); + isc_netaddr_fromin(&netaddr, &ina); + } else { + INSIST(rdata.length == sizeof(in6a.s6_addr)); + memcpy(in6a.s6_addr, rdata.data, sizeof(in6a.s6_addr)); + isc_netaddr_fromin6(&netaddr, &in6a); + } + + result = dns_acl_match(&netaddr, NULL, view->denyansweracl, + &view->aclenv, &match, NULL); + + if (result == ISC_R_SUCCESS && match > 0) { + isc_netaddr_format(&netaddr, addrbuf, sizeof(addrbuf)); + dns_name_format(name, namebuf, sizeof(namebuf)); + dns_rdatatype_format(rdataset->type, typebuf, + sizeof(typebuf)); + dns_rdataclass_format(rdataset->rdclass, classbuf, + sizeof(classbuf)); + isc_log_write(dns_lctx, DNS_LOGCATEGORY_RESOLVER, + DNS_LOGMODULE_RESOLVER, ISC_LOG_NOTICE, + "answer address %s denied for %s/%s/%s", + addrbuf, namebuf, typebuf, classbuf); + return (ISC_FALSE); + } + } + + return (ISC_TRUE); +} + +static isc_boolean_t +is_answertarget_allowed(dns_view_t *view, dns_name_t *name, + dns_rdatatype_t type, dns_name_t *tname, + dns_name_t *domain) +{ + isc_result_t result; + dns_rbtnode_t *node = NULL; + char qnamebuf[DNS_NAME_FORMATSIZE]; + char tnamebuf[DNS_NAME_FORMATSIZE]; + char classbuf[64]; + char typebuf[64]; + + /* By default, we allow any target name. */ + if (view->denyanswernames == NULL) + return (ISC_TRUE); + + /* + * If the owner name matches one in the exclusion list, either exactly + * or partially, allow it. + */ + if (view->answernames_exclude != NULL) { + result = dns_rbt_findnode(view->answernames_exclude, name, NULL, + &node, NULL, 0, NULL, NULL); + if (result == ISC_R_SUCCESS || result == DNS_R_PARTIALMATCH) + return (ISC_TRUE); + } + + /* + * If the target name is a subdomain of the search domain, allow it. + */ + if (dns_name_issubdomain(tname, domain)) + return (ISC_TRUE); + + /* + * Otherwise, apply filters. + */ + result = dns_rbt_findnode(view->denyanswernames, tname, NULL, &node, + NULL, 0, NULL, NULL); + if (result == ISC_R_SUCCESS || result == DNS_R_PARTIALMATCH) { + dns_name_format(name, qnamebuf, sizeof(qnamebuf)); + dns_name_format(tname, tnamebuf, sizeof(tnamebuf)); + dns_rdatatype_format(type, typebuf, sizeof(typebuf)); + dns_rdataclass_format(view->rdclass, classbuf, + sizeof(classbuf)); + isc_log_write(dns_lctx, DNS_LOGCATEGORY_RESOLVER, + DNS_LOGMODULE_RESOLVER, ISC_LOG_NOTICE, + "%s target %s denied for %s/%s", + typebuf, tnamebuf, qnamebuf, classbuf); + return (ISC_FALSE); + } + + return (ISC_TRUE); +} + /* * Handle a no-answer response (NXDOMAIN, NXRRSET, or referral). * If bind8_ns_resp is ISC_TRUE, this is a suspected BIND 8 @@ -5194,6 +5322,7 @@ answer_response(fetchctx_t *fctx) { unsigned int aflag; dns_rdatatype_t type; dns_fixedname_t dname, fqname; + dns_view_t *view; FCTXTRACE("answer_response"); @@ -5216,6 +5345,7 @@ answer_response(fetchctx_t *fctx) { aa = ISC_FALSE; qname = &fctx->name; type = fctx->type; + view = fctx->res->view; result = dns_message_firstname(message, DNS_SECTION_ANSWER); while (!done && result == ISC_R_SUCCESS) { name = NULL; @@ -5236,6 +5366,18 @@ answer_response(fetchctx_t *fctx) { */ return (DNS_R_FORMERR); } + + /* + * Apply filters, if given, on answers to reject + * a malicious attempt of rebinding. + */ + if ((rdataset->type == dns_rdatatype_a || + rdataset->type == dns_rdatatype_aaaa) && + !is_answeraddress_allowed(view, name, + rdataset)) { + return (DNS_R_SERVFAIL); + } + if (rdataset->type == type && !found_cname) { /* * We've found an ordinary answer. @@ -5284,6 +5426,14 @@ answer_response(fetchctx_t *fctx) { &tname); if (result != ISC_R_SUCCESS) return (result); + /* Apply filters on the target name. */ + if (!is_answertarget_allowed(view, + name, + rdataset->type, + &tname, + &fctx->domain)) { + return (DNS_R_SERVFAIL); + } } else if (rdataset->type == dns_rdatatype_rrsig && rdataset->covers == dns_rdatatype_cname @@ -5386,6 +5536,8 @@ answer_response(fetchctx_t *fctx) { rdataset != NULL; rdataset = ISC_LIST_NEXT(rdataset, link)) { isc_boolean_t found_dname = ISC_FALSE; + dns_name_t *dname_name; + found = ISC_FALSE; aflag = 0; if (rdataset->type == dns_rdatatype_dname) { @@ -5415,6 +5567,15 @@ answer_response(fetchctx_t *fctx) { return (result); else found_dname = ISC_TRUE; + + dname_name = dns_fixedname_name(&dname); + if (!is_answertarget_allowed(view, + qname, + rdataset->type, + dname_name, + &fctx->domain)) { + return (DNS_R_SERVFAIL); + } } else if (rdataset->type == dns_rdatatype_rrsig && rdataset->covers == dns_rdatatype_dname) { diff --git a/lib/dns/view.c b/lib/dns/view.c index f7a3662916..b5414d4cd3 100644 --- a/lib/dns/view.c +++ b/lib/dns/view.c @@ -15,7 +15,7 @@ * PERFORMANCE OF THIS SOFTWARE. */ -/* $Id: view.c,v 1.153 2009/01/27 22:29:59 jinmei Exp $ */ +/* $Id: view.c,v 1.154 2009/05/29 22:22:37 jinmei Exp $ */ /*! \file */ @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -178,6 +179,10 @@ dns_view_create(isc_mem_t *mctx, dns_rdataclass_t rdclass, view->notifyacl = NULL; view->updateacl = NULL; view->upfwdacl = NULL; + view->denyansweracl = NULL; + view->answeracl_exclude = NULL; + view->denyanswernames = NULL; + view->answernames_exclude = NULL; view->requestixfr = ISC_TRUE; view->provideixfr = ISC_TRUE; view->maxcachettl = 7 * 24 * 3600; @@ -313,6 +318,14 @@ destroy(dns_view_t *view) { dns_acl_detach(&view->updateacl); if (view->upfwdacl != NULL) dns_acl_detach(&view->upfwdacl); + if (view->denyansweracl != NULL) + dns_acl_detach(&view->denyansweracl); + if (view->answeracl_exclude != NULL) + dns_rbt_destroy(&view->answeracl_exclude); + if (view->denyanswernames != NULL) + dns_rbt_destroy(&view->denyanswernames); + if (view->answernames_exclude != NULL) + dns_rbt_destroy(&view->answernames_exclude); if (view->delonly != NULL) { dns_name_t *name; int i; diff --git a/lib/isccfg/namedconf.c b/lib/isccfg/namedconf.c index e2f4b45c84..5ec5b544c4 100644 --- a/lib/isccfg/namedconf.c +++ b/lib/isccfg/namedconf.c @@ -15,7 +15,7 @@ * PERFORMANCE OF THIS SOFTWARE. */ -/* $Id: namedconf.c,v 1.95 2009/03/04 02:42:31 each Exp $ */ +/* $Id: namedconf.c,v 1.96 2009/05/29 22:22:37 jinmei Exp $ */ /*! \file */ @@ -736,6 +736,34 @@ static cfg_type_t cfg_type_optional_exclude = { "optional_exclude", parse_optional_keyvalue, print_keyvalue, doc_optional_keyvalue, &cfg_rep_list, &exclude_kw }; +static keyword_type_t exceptionnames_kw = { "except-from", &cfg_type_namelist }; + +static cfg_type_t cfg_type_optional_exceptionnames = { + "optional_allow", parse_optional_keyvalue, print_keyvalue, + doc_optional_keyvalue, &cfg_rep_list, &exceptionnames_kw }; + +static cfg_tuplefielddef_t denyaddresses_fields[] = { + { "acl", &cfg_type_bracketed_aml, 0 }, + { "except-from", &cfg_type_optional_exceptionnames, 0 }, + { NULL, NULL, 0 } +}; + +static cfg_type_t cfg_type_denyaddresses = { + "denyaddresses", cfg_parse_tuple, cfg_print_tuple, cfg_doc_tuple, + &cfg_rep_tuple, denyaddresses_fields +}; + +static cfg_tuplefielddef_t denyaliases_fields[] = { + { "name", &cfg_type_namelist, 0 }, + { "except-from", &cfg_type_optional_exceptionnames, 0 }, + { NULL, NULL, 0 } +}; + +static cfg_type_t cfg_type_denyaliases = { + "denyaliases", cfg_parse_tuple, cfg_print_tuple, cfg_doc_tuple, + &cfg_rep_tuple, denyaliases_fields +}; + static cfg_type_t cfg_type_algorithmlist = { "algorithmlist", cfg_parse_bracketed_list, cfg_print_bracketed_list, cfg_doc_bracketed_list, &cfg_rep_list, &cfg_type_astring }; @@ -813,6 +841,8 @@ view_clauses[] = { { "check-names", &cfg_type_checknames, CFG_CLAUSEFLAG_MULTI }, { "cleaning-interval", &cfg_type_uint32, 0 }, { "clients-per-query", &cfg_type_uint32, 0 }, + { "deny-answer-addresses", &cfg_type_denyaddresses, 0 }, + { "deny-answer-aliases", &cfg_type_denyaliases, 0 }, { "disable-algorithms", &cfg_type_disablealgorithm, CFG_CLAUSEFLAG_MULTI }, { "disable-empty-zone", &cfg_type_astring, CFG_CLAUSEFLAG_MULTI },