Commit f240f4a5 authored by Mark Andrews's avatar Mark Andrews

Reimplement:

4578.   [security]      Some chaining (CNAME or DNAME) responses to upstream
                        queries could trigger assertion failures.
                        (CVE-2017-3137) [RT #44734]
parent ed5bf0e5
......@@ -21,6 +21,7 @@ a.short A 10.0.0.1
short-dname DNAME short
a.longlonglonglonglonglonglonglonglonglonglonglonglong A 10.0.0.2
long-dname DNAME longlonglonglonglonglonglonglonglonglonglonglonglong
toolong-dname DNAME longlonglonglonglonglonglonglonglonglonglonglonglong
cname CNAME a.cnamedname
cnamedname DNAME target
a.target A 10.0.0.3
......@@ -49,10 +49,19 @@ grep "status: YXDOMAIN" dig.out.ns2.toolong > /dev/null || ret=1
if [ $ret != 0 ]; then echo "I:failed"; fi
status=`expr $status + $ret`
echo "I:checking (too) long dname from recursive"
echo "I:checking (too) long dname from recursive with cached DNAME"
ret=0
$DIG 01234567890123456789012345678901234567890123456789.longlonglonglonglonglonglonglonglonglonglonglonglonglonglong.longlonglonglonglonglonglonglonglonglonglonglonglonglonglong.longlonglonglonglonglonglonglonglonglonglonglonglonglonglong.long-dname.example @10.53.0.4 a -p 5300 > dig.out.ns4.cachedtoolong || ret=1
grep "status: YXDOMAIN" dig.out.ns4.cachedtoolong > /dev/null || ret=1
grep '^long-dname\.example\..*DNAME.*long' dig.out.ns4.cachedtoolong > /dev/null || ret=1
if [ $ret != 0 ]; then echo "I:failed"; fi
status=`expr $status + $ret`
echo "I:checking (too) long dname from recursive without cached DNAME"
ret=0
$DIG 01234567890123456789012345678901234567890123456789.longlonglonglonglonglonglonglonglonglonglonglonglonglonglong.longlonglonglonglonglonglonglonglonglonglonglonglonglonglong.longlonglonglonglonglonglonglonglonglonglonglonglonglonglong.long-dname.example @10.53.0.4 a -p 5300 > dig.out.ns4.toolong || ret=1
grep "status: YXDOMAIN" dig.out.ns4.toolong > /dev/null || ret=1
$DIG 01234567890123456789012345678901234567890123456789.longlonglonglonglonglonglonglonglonglonglonglonglonglonglong.longlonglonglonglonglonglonglonglonglonglonglonglonglonglong.longlonglonglonglonglonglonglonglonglonglonglonglonglong.toolong-dname.example @10.53.0.4 a -p 5300 > dig.out.ns4.uncachedtoolong || ret=1
grep "status: YXDOMAIN" dig.out.ns4.uncachedtoolong > /dev/null || ret=1
grep '^toolong-dname\.example\..*DNAME.*long' dig.out.ns4.uncachedtoolong > /dev/null || ret=1
if [ $ret != 0 ]; then echo "I:failed"; fi
status=`expr $status + $ret`
......
......@@ -375,7 +375,7 @@ nxdomain a0-1s-cname.tld2s +dnssec @$ns6 # 19
drop a3-8.tld2 any @$ns6 # 20 drop
end_group
ckstatsrange $ns3 test1 ns3 22 25
ckstatsrange $ns3 test1 ns3 22 27
ckstats $ns5 test1 ns5 0
ckstats $ns6 test1 ns6 0
......
......@@ -4555,6 +4555,7 @@ is_lame(fetchctx_t *fctx) {
isc_result_t result;
if (message->rcode != dns_rcode_noerror &&
message->rcode != dns_rcode_yxdomain &&
message->rcode != dns_rcode_nxdomain)
return (ISC_FALSE);
......@@ -6179,79 +6180,6 @@ chase_additional(fetchctx_t *fctx) {
goto again;
}
static inline isc_result_t
cname_target(dns_rdataset_t *rdataset, dns_name_t *tname) {
isc_result_t result;
dns_rdata_t rdata = DNS_RDATA_INIT;
dns_rdata_cname_t cname;
result = dns_rdataset_first(rdataset);
if (result != ISC_R_SUCCESS)
return (result);
dns_rdataset_current(rdataset, &rdata);
result = dns_rdata_tostruct(&rdata, &cname, NULL);
if (result != ISC_R_SUCCESS)
return (result);
dns_name_init(tname, NULL);
dns_name_clone(&cname.cname, tname);
dns_rdata_freestruct(&cname);
return (ISC_R_SUCCESS);
}
/*%
* Construct the synthesised CNAME from the existing QNAME and
* the DNAME RR and store it in 'target'.
*/
static inline isc_result_t
dname_target(dns_rdataset_t *rdataset, dns_name_t *qname,
unsigned int nlabels, dns_name_t *target)
{
isc_result_t result;
dns_rdata_t rdata = DNS_RDATA_INIT;
dns_rdata_dname_t dname;
dns_fixedname_t prefix;
/*
* Get the target name of the DNAME.
*/
result = dns_rdataset_first(rdataset);
if (result != ISC_R_SUCCESS)
return (result);
dns_rdataset_current(rdataset, &rdata);
result = dns_rdata_tostruct(&rdata, &dname, NULL);
if (result != ISC_R_SUCCESS)
return (result);
dns_fixedname_init(&prefix);
dns_name_split(qname, nlabels, dns_fixedname_name(&prefix), NULL);
result = dns_name_concatenate(dns_fixedname_name(&prefix),
&dname.dname, target, NULL);
dns_rdata_freestruct(&dname);
return (result);
}
/*%
* Check if it was possible to construct 'qname' from 'lastcname'
* and 'rdataset'.
*/
static inline isc_result_t
fromdname(dns_rdataset_t *rdataset, dns_name_t *lastcname,
unsigned int nlabels, const dns_name_t *qname)
{
dns_fixedname_t fixed;
isc_result_t result;
dns_name_t *target;
dns_fixedname_init(&fixed);
target = dns_fixedname_name(&fixed);
result = dname_target(rdataset, lastcname, nlabels, target);
if (result != ISC_R_SUCCESS || !dns_name_equal(qname, target))
return (ISC_R_NOTFOUND);
return (ISC_R_SUCCESS);
}
static isc_boolean_t
is_answeraddress_allowed(dns_view_t *view, dns_name_t *name,
dns_rdataset_t *rdataset)
......@@ -6327,9 +6255,8 @@ is_answeraddress_allowed(dns_view_t *view, dns_name_t *name,
}
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)
is_answertarget_allowed(fetchctx_t *fctx, dns_name_t *qname, dns_name_t *rname,
dns_rdataset_t *rdataset)
{
isc_result_t result;
dns_rbtnode_t *node = NULL;
......@@ -6337,18 +6264,58 @@ is_answertarget_allowed(dns_view_t *view, dns_name_t *name,
char tnamebuf[DNS_NAME_FORMATSIZE];
char classbuf[64];
char typebuf[64];
dns_name_t *tname = NULL;
dns_rdata_cname_t cname;
dns_rdata_dname_t dname;
dns_view_t *view = fctx->res->view;
dns_rdata_t rdata = DNS_RDATA_INIT;
unsigned int nlabels;
dns_fixedname_t fixed;
dns_name_t prefix;
REQUIRE(rdataset != NULL);
REQUIRE(rdataset->type == dns_rdatatype_cname ||
rdataset->type == dns_rdatatype_dname);
/* By default, we allow any target name. */
if (view->denyanswernames == NULL)
return (ISC_TRUE);
result = dns_rdataset_first(rdataset);
RUNTIME_CHECK(result == ISC_R_SUCCESS);
dns_rdataset_current(rdataset, &rdata);
switch (rdataset->type) {
case dns_rdatatype_cname:
result = dns_rdata_tostruct(&rdata, &cname, NULL);
RUNTIME_CHECK(result == ISC_R_SUCCESS);
tname = &cname.cname;
break;
case dns_rdatatype_dname:
result = dns_rdata_tostruct(&rdata, &dname, NULL);
RUNTIME_CHECK(result == ISC_R_SUCCESS);
dns_name_init(&prefix, NULL);
dns_fixedname_init(&fixed);
tname = dns_fixedname_name(&fixed);
nlabels = dns_name_countlabels(qname) -
dns_name_countlabels(rname);
dns_name_split(qname, nlabels, &prefix, NULL);
result = dns_name_concatenate(&prefix, &dname.dname, tname,
NULL);
if (result == ISC_R_NOSPACE)
return (ISC_TRUE);
RUNTIME_CHECK(result == ISC_R_SUCCESS);
break;
default:
INSIST(0);
}
/*
* 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);
result = dns_rbt_findnode(view->answernames_exclude, qname,
NULL, &node, NULL, 0, NULL, NULL);
if (result == ISC_R_SUCCESS || result == DNS_R_PARTIALMATCH)
return (ISC_TRUE);
}
......@@ -6356,7 +6323,7 @@ is_answertarget_allowed(dns_view_t *view, dns_name_t *name,
/*
* If the target name is a subdomain of the search domain, allow it.
*/
if (dns_name_issubdomain(tname, domain))
if (dns_name_issubdomain(tname, &fctx->domain))
return (ISC_TRUE);
/*
......@@ -6365,9 +6332,9 @@ is_answertarget_allowed(dns_view_t *view, dns_name_t *name,
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(qname, qnamebuf, sizeof(qnamebuf));
dns_name_format(tname, tnamebuf, sizeof(tnamebuf));
dns_rdatatype_format(type, typebuf, sizeof(typebuf));
dns_rdatatype_format(rdataset->type, typebuf, sizeof(typebuf));
dns_rdataclass_format(view->rdclass, classbuf,
sizeof(classbuf));
isc_log_write(dns_lctx, DNS_LOGCATEGORY_RESOLVER,
......@@ -6855,459 +6822,297 @@ noanswer_response(fetchctx_t *fctx, dns_name_t *oqname,
return (ISC_R_SUCCESS);
}
static isc_boolean_t
validinanswer(dns_rdataset_t *rdataset, fetchctx_t *fctx) {
if (rdataset->type == dns_rdatatype_nsec3) {
/*
* NSEC3 records are not allowed to
* appear in the answer section.
*/
log_formerr(fctx, "NSEC3 in answer");
return (ISC_FALSE);
}
if (rdataset->type == dns_rdatatype_tkey) {
/*
* TKEY is not a valid record in a
* response to any query we can make.
*/
log_formerr(fctx, "TKEY in answer");
return (ISC_FALSE);
}
if (rdataset->rdclass != fctx->res->rdclass) {
log_formerr(fctx, "Mismatched class in answer");
return (ISC_FALSE);
}
return (ISC_TRUE);
}
static isc_result_t
answer_response(fetchctx_t *fctx) {
isc_result_t result;
dns_message_t *message;
dns_name_t *name, *dname = NULL, *qname, tname, *ns_name;
dns_name_t *cname = NULL, *lastcname = NULL;
dns_rdataset_t *rdataset, *ns_rdataset;
isc_boolean_t done, external, aa, found, want_chaining;
isc_boolean_t have_answer, found_cname, found_dname, found_type;
isc_boolean_t wanted_chaining;
unsigned int aflag, chaining;
dns_message_t *message = NULL;
dns_name_t *name = NULL, *qname = NULL, *ns_name = NULL;
dns_name_t *aname = NULL, *cname = NULL, *dname = NULL;
dns_rdataset_t *rdataset = NULL, *sigrdataset = NULL;
dns_rdataset_t *ardataset = NULL, *crdataset = NULL;
dns_rdataset_t *drdataset = NULL, *ns_rdataset = NULL;
isc_boolean_t done = ISC_FALSE, aa;
unsigned int dname_labels, domain_labels;
isc_boolean_t chaining = ISC_FALSE;
dns_rdatatype_t type;
dns_fixedname_t fdname, fqname;
dns_view_t *view;
dns_view_t *view = NULL;
dns_trust_t trust;
REQUIRE(VALID_FCTX(fctx));
FCTXTRACE("answer_response");
message = fctx->rmessage;
qname = &fctx->name;
view = fctx->res->view;
type = fctx->type;
/*
* Examine the answer section, marking those rdatasets which are
* part of the answer and should be cached.
* There can be multiple RRSIG and SIG records at a name so
* we treat these types as a subset of ANY.
*/
if (type == dns_rdatatype_rrsig || type == dns_rdatatype_sig) {
type = dns_rdatatype_any;
}
done = ISC_FALSE;
found_cname = ISC_FALSE;
found_dname = ISC_FALSE;
found_type = ISC_FALSE;
have_answer = ISC_FALSE;
want_chaining = ISC_FALSE;
chaining = 0;
POST(want_chaining);
if ((message->flags & DNS_MESSAGEFLAG_AA) != 0)
aa = ISC_TRUE;
else
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) {
dns_namereln_t namereln, lastreln;
int order, lastorder;
unsigned int nlabels, lastnlabels;
/*
* Bigger than any valid DNAME label count.
*/
dname_labels = dns_name_countlabels(qname);
domain_labels = dns_name_countlabels(&fctx->domain);
/*
* Perform a single pass looking for the answer, cname or covering
* dname.
*/
for (result = dns_message_firstname(message, DNS_SECTION_ANSWER);
result == ISC_R_SUCCESS;
result = dns_message_nextname(message, DNS_SECTION_ANSWER))
{
int order;
unsigned int nlabels;
dns_namereln_t namereln;
name = NULL;
dns_message_currentname(message, DNS_SECTION_ANSWER, &name);
external = ISC_TF(!dns_name_issubdomain(name, &fctx->domain));
namereln = dns_name_fullcompare(qname, name, &order, &nlabels);
if (namereln == dns_namereln_equal) {
wanted_chaining = ISC_FALSE;
switch (namereln) {
case dns_namereln_equal:
for (rdataset = ISC_LIST_HEAD(name->list);
rdataset != NULL;
rdataset = ISC_LIST_NEXT(rdataset, link)) {
found = ISC_FALSE;
want_chaining = ISC_FALSE;
aflag = 0;
if (rdataset->type == dns_rdatatype_nsec3) {
/*
* NSEC3 records are not allowed to
* appear in the answer section.
*/
log_formerr(fctx, "NSEC3 in answer");
return (DNS_R_FORMERR);
}
if (rdataset->type == dns_rdatatype_tkey) {
/*
* TKEY is not a valid record in a
* response to any query we can make.
*/
log_formerr(fctx, "TKEY in answer");
return (DNS_R_FORMERR);
}
if (rdataset->rdclass != fctx->res->rdclass) {
log_formerr(fctx, "Mismatched class "
"in answer");
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.
*/
found = ISC_TRUE;
found_type = ISC_TRUE;
done = ISC_TRUE;
aflag = DNS_RDATASETATTR_ANSWER;
} else if (type == dns_rdatatype_any) {
/*
* We've found an answer matching
* an ANY query. There may be
* more.
*/
found = ISC_TRUE;
aflag = DNS_RDATASETATTR_ANSWER;
} else if (rdataset->type == dns_rdatatype_rrsig
&& rdataset->covers == type
&& !found_cname) {
/*
* We've found a signature that
* covers the type we're looking for.
*/
found = ISC_TRUE;
found_type = ISC_TRUE;
aflag = DNS_RDATASETATTR_ANSWERSIG;
} else if (rdataset->type ==
dns_rdatatype_cname
&& !found_type) {
/*
* We're looking for something else,
* but we found a CNAME.
*
* Getting a CNAME response for some
* query types is an error, see
* RFC 4035, Section 2.5.
*/
if (type == dns_rdatatype_rrsig ||
type == dns_rdatatype_key ||
type == dns_rdatatype_nsec) {
char buf[DNS_RDATATYPE_FORMATSIZE];
dns_rdatatype_format(fctx->type,
buf, sizeof(buf));
log_formerr(fctx,
"CNAME response "
"for %s RR", buf);
return (DNS_R_FORMERR);
}
found = ISC_TRUE;
found_cname = ISC_TRUE;
want_chaining = ISC_TRUE;
aflag = DNS_RDATASETATTR_ANSWER;
result = cname_target(rdataset,
&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);
rdataset = ISC_LIST_NEXT(rdataset, link))
{
if (rdataset->type == type ||
type == dns_rdatatype_any)
{
aname = name;
if (type != dns_rdatatype_any) {
ardataset = rdataset;
}
lastcname = name;
} else if (rdataset->type == dns_rdatatype_rrsig
&& rdataset->covers ==
dns_rdatatype_cname
&& !found_type) {
/*
* We're looking for something else,
* but we found a SIG CNAME.
*/
found = ISC_TRUE;
found_cname = ISC_TRUE;
aflag = DNS_RDATASETATTR_ANSWERSIG;
break;
}
if (found) {
/*
* We've found an answer to our
* question.
*/
name->attributes |=
DNS_NAMEATTR_CACHE;
rdataset->attributes |=
DNS_RDATASETATTR_CACHE;
rdataset->trust = dns_trust_answer;
if (external) {
/*
* This data is outside of
* our query domain, and
* may not be cached.
*/
rdataset->attributes |=
DNS_RDATASETATTR_EXTERNAL;
} else if (chaining == 0) {
/*
* Don't use found_cname here
* as we have just set it
* above.
*/
if (cname == NULL &&
!found_dname &&
aflag ==
DNS_RDATASETATTR_ANSWER)
{
have_answer = ISC_TRUE;
if (found_cname &&
cname == NULL)
cname = name;
name->attributes |=
DNS_NAMEATTR_ANSWER;
}
rdataset->attributes |= aflag;
if (aa)
rdataset->trust =
dns_trust_authanswer;
}
/*
* Mark any additional data related
* to this rdataset.
*/
(void)dns_rdataset_additionaldata(
rdataset,
check_related,
fctx);
/*
* CNAME chaining.
*/
if (want_chaining) {
wanted_chaining = ISC_TRUE;
name->attributes |=
DNS_NAMEATTR_CHAINING;
rdataset->attributes |=
DNS_RDATASETATTR_CHAINING;
qname = &tname;
}
if (rdataset->type == dns_rdatatype_cname) {
cname = name;
crdataset = rdataset;
break;
}
/*
* We could add an "else" clause here and
* log that we're ignoring this rdataset.
*/
}
break;
case dns_namereln_subdomain:
/*
* If wanted_chaining is true, we've done
* some chaining as the result of processing
* this node, and thus we need to set
* chaining to true.
*
* We don't set chaining inside of the
* rdataset loop because doing that would
* cause us to ignore the signatures of
* CNAMEs.
* In-scope DNAME records must have at least
* as many labels as the domain being queried.
* They also must be less that qname's labels
* and any previously found dname.
*/
if (wanted_chaining && chaining < 2U)
chaining++;
} else {
dns_rdataset_t *dnameset = NULL;
isc_boolean_t synthcname = ISC_FALSE;
if (lastcname != NULL) {
lastreln = dns_name_fullcompare(lastcname,
name,
&lastorder,
&lastnlabels);
if (lastreln == dns_namereln_subdomain &&
lastnlabels == dns_name_countlabels(name))
synthcname = ISC_TRUE;
if (nlabels >= dname_labels || nlabels < domain_labels)
{
continue;
}
/*
* Look for a DNAME (or its SIG). Anything else is
* ignored.
* We are looking for the shortest DNAME if there
* are multiple ones (which there shouldn't be).
*/
wanted_chaining = ISC_FALSE;
for (rdataset = ISC_LIST_HEAD(name->list);
rdataset != NULL;
rdataset = ISC_LIST_NEXT(rdataset, link))
{
if (rdataset->rdclass != fctx->res->rdclass) {
log_formerr(fctx, "Mismatched class "
"in answer");
return (DNS_R_FORMERR);
}
/*
* Only pass DNAME or RRSIG(DNAME).
*/
if (rdataset->type != dns_rdatatype_dname &&
(rdataset->type != dns_rdatatype_rrsig ||
rdataset->covers != dns_rdatatype_dname))
if (rdataset->type != dns_rdatatype_dname) {
continue;
/*
* If we're not chaining, then the DNAME and
* its signature should not be external.
*/
if (chaining == 0 && external) {
char qbuf[DNS_NAME_FORMATSIZE];
char obuf[DNS_NAME_FORMATSIZE];
dns_name_format(name, qbuf,
sizeof(qbuf));
dns_name_format(&fctx->domain, obuf,
sizeof(obuf));
log_formerr(fctx, "external DNAME or "
"RRSIG covering DNAME "
"in answer: %s is "
"not in %s", qbuf, obuf);
return (DNS_R_FORMERR);
}
dname = name;
drdataset = rdataset;
dname_labels = nlabels;
break;
}
break;
default:
break;
}
}
/*
* If DNAME + synthetic CNAME then the
* namereln is dns_namereln_subdomain.
*/
if (