fix: usr: validate adb fetches

ADB responses were not being validated, allowing spoofed responses to be accepted and used for further lookups. This should not be possible when the servers for the zone are in a signed zone, except with CD=1 requests or when glue is needed. This has been fixed.

Closes #5066

Merge branch '5066-validate-adb-fetches' into 'main'

See merge request isc-projects/bind9!10052
This commit is contained in:
Mark Andrews 2025-02-03 02:18:09 +00:00
commit d9eb272b69
14 changed files with 198 additions and 18 deletions

View file

@ -37,3 +37,5 @@ inprogress. NS ns10.inprogress.
ns10.inprogress. A 10.53.0.10
too-many-iterations. NS ns2.too-many-iterations.
ns2.too-many-iterations. A 10.53.0.2
peer-ns-spoof NS ns2.peer-ns-spoof.
ns2.peer-ns-spoof. A 10.53.0.2

View file

@ -30,6 +30,7 @@ cp "../ns2/dsset-example." .
cp "../ns2/dsset-in-addr.arpa." .
cp "../ns2/dsset-too-many-iterations." .
cp "../ns2/dsset-lazy-ksk." .
cp "../ns2/dsset-peer-ns-spoof." .
grep "$DEFAULT_ALGORITHM_NUMBER [12] " "../ns2/dsset-algroll." >"dsset-algroll."
cp "../ns6/dsset-optout-tld." .

View file

@ -214,4 +214,14 @@ zone "lazy-ksk" {
allow-update { any; };
};
zone "peer-ns-spoof" {
type primary;
file "peer-ns-spoof.db.signed";
};
zone "peer.peer-ns-spoof" {
type primary;
file "peer.peer-ns-spoof.db.signed";
};
include "trusted.conf";

View file

@ -0,0 +1,24 @@
; Copyright (C) Internet Systems Consortium, Inc. ("ISC")
;
; SPDX-License-Identifier: MPL-2.0
;
; This Source Code Form is subject to the terms of the Mozilla Public
; License, v. 2.0. If a copy of the MPL was not distributed with this
; file, you can obtain one at https://mozilla.org/MPL/2.0/.
;
; See the COPYRIGHT file distributed with this work for additional
; information regarding copyright ownership.
$TTL 300 ; 5 minutes
@ IN SOA mname1. . (
2000042407 ; serial
20 ; refresh (20 seconds)
20 ; retry (20 seconds)
1814400 ; expire (3 weeks)
3600 ; minimum (1 hour)
)
NS ns2
ns2 A 10.53.0.2
peer NS ns2.peer
ns2.peer A 10.53.0.2
target NS ns3.peer

View file

@ -0,0 +1,22 @@
; Copyright (C) Internet Systems Consortium, Inc. ("ISC")
;
; SPDX-License-Identifier: MPL-2.0
;
; This Source Code Form is subject to the terms of the Mozilla Public
; License, v. 2.0. If a copy of the MPL was not distributed with this
; file, you can obtain one at https://mozilla.org/MPL/2.0/.
;
; See the COPYRIGHT file distributed with this work for additional
; information regarding copyright ownership.
$TTL 300 ; 5 minutes
@ IN SOA mname1. . (
2000042407 ; serial
20 ; refresh (20 seconds)
20 ; retry (20 seconds)
1814400 ; expire (3 weeks)
3600 ; minimum (1 hour)
)
NS ns2
ns2 A 10.53.0.2
ns3 A 10.53.0.3

View file

@ -27,6 +27,8 @@ for subdomain in secure unsupported disabled enabled; do
cp "../ns3/dsset-$subdomain.trusted." .
done
cp "../ns3/dsset-target.peer-ns-spoof." .
# Sign the "trusted." and "managed." zones.
zone=managed.
infile=key.db.in
@ -354,3 +356,34 @@ rm "$rm1.key"
rm "$rm1.private"
rm "$rm2.key"
rm "$rm2.private"
#
# A zone with where the address for peer zone server is modified and signatures
# stripped.
#
zone=peer.peer-ns-spoof
infile=peer.peer-ns-spoof.db.in
zonefile=peer.peer-ns-spoof.db
ksk=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" -n zone -f KSK "$zone")
zsk=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" -n zone "$zone")
cat "$infile" "$ksk.key" "$zsk.key" >"$zonefile"
"$SIGNER" -g -o "$zone" "$zonefile" >/dev/null 2>&1
"$CHECKZONE" -D -q -i local "$zone" "$zonefile.signed" \
| awk '$1 == "ns3.peer.peer-ns-spoof." && $4 == "RRSIG" && $5 == "A" { next }
$1 == "ns3.peer.peer-ns-spoof." && $4 == "A" { $5 = "10.53.0.100" }
{ print }' >"$zonefile.stripped"
"$CHECKZONE" -D -q -i local "$zone" "$zonefile.signed" \
| awk '$4 == "SOA" { $7 = $7 + 1; print; next } { print }' >"$zonefile.next"
"$SIGNER" -g -o "$zone" -f "$zonefile.next" "$zonefile.next" >/dev/null 2>&1
cp "$zonefile.stripped" "$zonefile.signed"
#
# parent zone for peer.peer-ns-spoof
#
zone=peer-ns-spoof
infile=peer-ns-spoof.db.in
zonefile=peer-ns-spoof.db
ksk=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" -n zone -f KSK "$zone")
zsk=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" -n zone "$zone")
cat "$infile" "$ksk.key" "$zsk.key" >"$zonefile"
"$SIGNER" -g -o "$zone" "$zonefile" >/dev/null 2>&1

View file

@ -423,6 +423,11 @@ zone "rsasha1-1024.example" {
file "rsasha1-1024.example.db";
};
zone "target.peer-ns-spoof" {
type primary;
file "target.peer-ns-spoof.db.signed";
};
dnssec-policy "siginterval1" {
keys {
ksk key-directory lifetime unlimited algorithm @DEFAULT_ALGORITHM@;

View file

@ -701,3 +701,14 @@ zone=rsasha1-1024.example
zonefile=rsasha1-1024.example.db
awk '$4 == "DNSKEY" && $5 == 257 { print }' "$zonefile" \
| $DSFROMKEY -f - "$zone" >"dsset-${zone}."
#
#
#
zone=target.peer-ns-spoof
infile=target.peer-ns-spoof.db.in
zonefile=target.peer-ns-spoof.db
ksk=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" -n zone -f KSK "$zone")
zsk=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" -n zone "$zone")
cat "$infile" "$ksk.key" "$zsk.key" >"$zonefile"
"$SIGNER" -g -o "$zone" "$zonefile" >/dev/null 2>&1

View file

@ -0,0 +1,20 @@
; Copyright (C) Internet Systems Consortium, Inc. ("ISC")
;
; SPDX-License-Identifier: MPL-2.0
;
; This Source Code Form is subject to the terms of the Mozilla Public
; License, v. 2.0. If a copy of the MPL was not distributed with this
; file, you can obtain one at https://mozilla.org/MPL/2.0/.
;
; See the COPYRIGHT file distributed with this work for additional
; information regarding copyright ownership.
$TTL 300 ; 5 minutes
@ IN SOA mname1. . (
2000042407 ; serial
20 ; refresh (20 seconds)
20 ; retry (20 seconds)
1814400 ; expire (3 weeks)
3600 ; minimum (1 hour)
)
NS ns3.peer.peer-ns-spoof.

View file

@ -184,6 +184,26 @@ n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "checking recovery from spoofed server addresses ($n)"
ret=0
# prime cache with spoofed address records
dig_with_opts +cd target.peer-ns-spoof @10.53.0.4 a >dig.out.prime.ns4.test$n || ret=1
grep "status: SERVFAIL" dig.out.prime.ns4.test$n >/dev/null || ret=1
rndccmd 10.53.0.4 dumpdb | sed 's/^/ns4 /' | cat_i
mv ns4/named_dump.db ns4/named_dump.db.test$n >/dev/null || ret=1
grep "10.53.0.100" ns4/named_dump.db.test$n || ret=1
# reload server with properly signed zone
cp ns2/peer.peer-ns-spoof.db.next ns2/peer.peer-ns-spoof.db.signed
nextpart ns2/named.run >/dev/null
rndccmd 10.53.0.2 reload peer.peer-ns-spoof | sed 's/^/ns2 /' | cat_i
wait_for_log 5 "zone peer.peer-ns-spoof/IN: loaded serial 2000042408" ns2/named.run || ret=1
dig_with_opts +noauth test.target.peer-ns-spoof @10.53.0.4 txt >dig.out.ns4.test$n || ret=1
grep "status: NXDOMAIN" dig.out.ns4.test$n >/dev/null || ret=1
grep "flags: qr rd ra ad;" dig.out.ns4.test$n >/dev/null || ret=1
n=$((n + 1))
test "$ret" -eq 0 || echo_i "failed"
status=$((status + ret))
echo_i "checking that 'example/DS' from the referral was used in previous validation ($n)"
ret=0
grep "query 'example/DS/IN' approved" ns1/named.run >/dev/null && ret=1

View file

@ -55,6 +55,10 @@ pytestmark = pytest.mark.extra_artifacts(
"ns2/lazy-ksk.db",
"ns2/managed.db",
"ns2/nsec3chain-test.db",
"ns2/peer-ns-spoof.db",
"ns2/peer.peer-ns-spoof.db",
"ns2/peer.peer-ns-spoof.db.next",
"ns2/peer.peer-ns-spoof.db.stripped",
"ns2/settime.out.updatecheck-kskonly.secure.ksk",
"ns2/settime.out.updatecheck-kskonly.secure.zsk",
"ns2/single-nsec3.db",
@ -121,6 +125,7 @@ pytestmark = pytest.mark.extra_artifacts(
"ns3/siginterval.example.db",
"ns3/split-dnssec.example.db",
"ns3/split-smart.example.db",
"ns3/target.peer-ns-spoof.db",
"ns3/trusted-future.key",
"ns3/ttlpatch.example.db",
"ns3/ttlpatch.example.db.patched",
@ -136,6 +141,7 @@ pytestmark = pytest.mark.extra_artifacts(
"ns4/managed.conf",
"ns4/managed-keys.bind",
"ns4/named.secroots",
"ns4/named_dump.db",
"ns4/named_dump.db.*",
"ns5/revoked.conf",
"ns5/trusted.conf",

View file

@ -332,7 +332,7 @@ expire_entry(dns_adbentry_t *adbentry);
static isc_result_t
dbfind_name(dns_adbname_t *, isc_stdtime_t, dns_rdatatype_t);
static isc_result_t
fetch_name(dns_adbname_t *, bool, unsigned int, isc_counter_t *qc,
fetch_name(dns_adbname_t *, bool, bool, unsigned int, isc_counter_t *qc,
isc_counter_t *gqc, dns_rdatatype_t);
static void
destroy(dns_adb_t *);
@ -410,10 +410,13 @@ enum {
#define FIND_AVOIDFETCHES(fn) (((fn)->options & DNS_ADBFIND_AVOIDFETCHES) != 0)
#define FIND_STARTATZONE(fn) (((fn)->options & DNS_ADBFIND_STARTATZONE) != 0)
#define FIND_STATICSTUB(fn) (((fn)->options & DNS_ADBFIND_STATICSTUB) != 0)
#define FIND_NOVALIDATE(fn) (((fn)->options & DNS_ADBFIND_NOVALIDATE) != 0)
#define FIND_HAS_ADDRS(fn) (!ISC_LIST_EMPTY((fn)->list))
#define FIND_NOFETCH(fn) (((fn)->options & DNS_ADBFIND_NOFETCH) != 0)
#define ADBNAME_FLAGS_MASK (DNS_ADBFIND_STARTATZONE | DNS_ADBFIND_STATICSTUB)
#define ADBNAME_FLAGS_MASK \
(DNS_ADBFIND_STARTATZONE | DNS_ADBFIND_STATICSTUB | \
DNS_ADBFIND_NOVALIDATE)
/*
* These are currently used on simple unsigned ints, so they are
@ -555,6 +558,8 @@ import_rdataset(dns_adbname_t *adbname, dns_rdataset_t *rdataset,
switch (rdataset->trust) {
case dns_trust_glue:
case dns_trust_additional:
case dns_trust_pending_answer:
case dns_trust_pending_additional:
rdataset->ttl = ADB_CACHE_MINIMUM;
break;
case dns_trust_ultimate:
@ -2118,6 +2123,8 @@ fetch:
if (wanted_fetches != 0 && !(FIND_AVOIDFETCHES(find) && have_address) &&
!FIND_NOFETCH(find))
{
bool no_validate = FIND_NOVALIDATE(find);
/*
* We're missing at least one address family. Either the
* caller hasn't instructed us to avoid fetches, or we don't
@ -2133,8 +2140,8 @@ fetch:
* Start V4.
*/
if (WANT_INET(wanted_fetches) &&
fetch_name(adbname, start_at_zone, depth, qc, gqc,
dns_rdatatype_a) == ISC_R_SUCCESS)
fetch_name(adbname, start_at_zone, no_validate, depth, qc,
gqc, dns_rdatatype_a) == ISC_R_SUCCESS)
{
DP(DEF_LEVEL,
"dns_adb_createfind: "
@ -2146,8 +2153,8 @@ fetch:
* Start V6.
*/
if (WANT_INET6(wanted_fetches) &&
fetch_name(adbname, start_at_zone, depth, qc, gqc,
dns_rdatatype_aaaa) == ISC_R_SUCCESS)
fetch_name(adbname, start_at_zone, no_validate, depth, qc,
gqc, dns_rdatatype_aaaa) == ISC_R_SUCCESS)
{
DP(DEF_LEVEL,
"dns_adb_createfind: "
@ -2642,6 +2649,7 @@ dbfind_name(dns_adbname_t *adbname, isc_stdtime_t now, dns_rdatatype_t rdtype) {
dns_adb_t *adb = NULL;
dns_fixedname_t foundname;
dns_name_t *fname = NULL;
unsigned int options = DNS_DBFIND_GLUEOK | DNS_DBFIND_ADDITIONALOK;
REQUIRE(DNS_ADBNAME_VALID(adbname));
@ -2667,11 +2675,13 @@ dbfind_name(dns_adbname_t *adbname, isc_stdtime_t now, dns_rdatatype_t rdtype) {
* any matching static-stub zone without looking into the cache to honor
* the configuration on which server we should send queries to.
*/
result =
dns_view_find(adb->view, adbname->name, rdtype, now,
DNS_DBFIND_GLUEOK | DNS_DBFIND_ADDITIONALOK, true,
((adbname->flags & DNS_ADBFIND_STARTATZONE) != 0),
NULL, NULL, fname, &rdataset, NULL);
if ((adbname->flags & DNS_ADBFIND_STARTATZONE) != 0) {
options |= DNS_DBFIND_PENDINGOK;
}
result = dns_view_find(
adb->view, adbname->name, rdtype, now, options, true,
((adbname->flags & DNS_ADBFIND_STARTATZONE) != 0), NULL, NULL,
fname, &rdataset, NULL);
switch (result) {
case DNS_R_GLUE:
@ -2951,8 +2961,9 @@ out:
}
static isc_result_t
fetch_name(dns_adbname_t *adbname, bool start_at_zone, unsigned int depth,
isc_counter_t *qc, isc_counter_t *gqc, dns_rdatatype_t type) {
fetch_name(dns_adbname_t *adbname, bool start_at_zone, bool no_validation,
unsigned int depth, isc_counter_t *qc, isc_counter_t *gqc,
dns_rdatatype_t type) {
isc_result_t result;
dns_adbfetch_t *fetch = NULL;
dns_adb_t *adb = NULL;
@ -2960,7 +2971,7 @@ fetch_name(dns_adbname_t *adbname, bool start_at_zone, unsigned int depth,
dns_name_t *name = NULL;
dns_rdataset_t rdataset;
dns_rdataset_t *nameservers = NULL;
unsigned int options;
unsigned int options = no_validation ? DNS_FETCHOPT_NOVALIDATE : 0;
REQUIRE(DNS_ADBNAME_VALID(adbname));
@ -2975,8 +2986,6 @@ fetch_name(dns_adbname_t *adbname, bool start_at_zone, unsigned int depth,
dns_rdataset_init(&rdataset);
options = DNS_FETCHOPT_NOVALIDATE;
if (start_at_zone) {
DP(ENTER_LEVEL, "fetch_name: starting at zone for name %p",
adbname);
@ -3411,6 +3420,7 @@ dns_adb_flushname(dns_adb_t *adb, const dns_name_t *name) {
isc_result_t result;
bool start_at_zone = false;
bool static_stub = false;
bool novalidate = false;
dns_adbname_t key = { .name = UNCONST(name) };
REQUIRE(DNS_ADB_VALID(adb));
@ -3424,10 +3434,12 @@ dns_adb_flushname(dns_adb_t *adb, const dns_name_t *name) {
again:
/*
* Delete all entries - with and without DNS_ADBFIND_STARTATZONE set
* and with and without DNS_ADBFIND_STATICSTUB set.
* with and without DNS_ADBFIND_STATICSTUB set and with and without
* DNS_ADBFIND_NOVALIDATE set.
*/
key.flags = ((static_stub) ? DNS_ADBFIND_STATICSTUB : 0) |
((start_at_zone) ? DNS_ADBFIND_STARTATZONE : 0);
((start_at_zone) ? DNS_ADBFIND_STARTATZONE : 0) |
((novalidate) ? DNS_ADBFIND_NOVALIDATE : 0);
result = isc_hashmap_find(adb->names, hash_adbname(&key), match_adbname,
(void *)&key, (void **)&adbname);
@ -3448,6 +3460,12 @@ again:
static_stub = true;
goto again;
}
if (!novalidate) {
start_at_zone = false;
static_stub = false;
novalidate = true;
goto again;
}
RWUNLOCK(&adb->names_lock, isc_rwlocktype_write);
}

View file

@ -193,6 +193,7 @@ struct dns_adbfind {
* Only look for glue record for static stub.
*/
#define DNS_ADBFIND_STATICSTUB 0x00001000
#define DNS_ADBFIND_NOVALIDATE 0x00002000
/*%
* The answers to queries come back as a list of these.

View file

@ -3265,6 +3265,13 @@ findname(fetchctx_t *fctx, const dns_name_t *name, in_port_t port,
options |= DNS_ADBFIND_QUOTAEXEMPT;
}
/*
* Pass through NOVALIDATE to any lookups ADB makes.
*/
if ((fctx->options & DNS_FETCHOPT_NOVALIDATE) != 0) {
options |= DNS_ADBFIND_NOVALIDATE;
}
/*
* See what we know about this address.
*/