From: Vladimír Čunát Date: Wed, 17 Aug 2022 14:34:06 +0000 (+0200) Subject: lib/zonecut + iterator: limit large NS sets X-Git-Tag: v5.5.3^2~1 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f6577a20e493c7fbdac124d7544bf1846b084185;p=thirdparty%2Fknot-resolver.git lib/zonecut + iterator: limit large NS sets It's a mitigation for CVE-2022-40188 and similar DoS attempts. It's using really trivial approaches, at least for now. --- diff --git a/NEWS b/NEWS index 56a426359..4add63db5 100644 --- a/NEWS +++ b/NEWS @@ -1,6 +1,10 @@ Knot Resolver 5.x.y (2022-mm-dd) ================================ +Security +-------- +- fix CPU-expensive DoS by malicious domains - CVE-2022-40188 + Improvements ------------ - fix config_tests on macOS (both HW variants) diff --git a/lib/layer/iterate.c b/lib/layer/iterate.c index b1c3df40f..a73089932 100644 --- a/lib/layer/iterate.c +++ b/lib/layer/iterate.c @@ -187,6 +187,8 @@ static int update_nsaddr(const knot_rrset_t *rr, struct kr_query *query, int *gl return KR_STATE_CONSUME; } +enum { GLUE_COUNT_THROTTLE = 26 }; + /** @internal From \a pkt, fetch glue records for name \a ns, and update the cut etc. * * \param glue_cnt the number of accepted addresses (to be incremented) @@ -223,6 +225,10 @@ static void fetch_glue(knot_pkt_t *pkt, const knot_dname_t *ns, bool in_bailiwic continue; } (void) update_nsaddr(rr, req->current_query, glue_cnt); + /* If we reach limit on total glue addresses, + * we only load the first one per NS name (the one just above). */ + if (*glue_cnt > GLUE_COUNT_THROTTLE) + break; } } } @@ -425,6 +431,7 @@ static int process_authority(knot_pkt_t *pkt, struct kr_request *req) const knot_dname_t *current_zone_cut = qry->zone_cut.name; bool ns_record_exists = false; int glue_cnt = 0; + int ns_count = 0; /* Update zone cut information. */ for (unsigned i = 0; i < ns->count; ++i) { const knot_rrset_t *rr = knot_pkt_rr(ns, i); @@ -436,6 +443,11 @@ static int process_authority(knot_pkt_t *pkt, struct kr_request *req) case KR_STATE_FAIL: return state; break; default: /* continue */ break; } + + if (++ns_count >= 13) { + VERBOSE_MSG("<= authority: many glue NSs, skipping the rest\n"); + break; + } } else if (rr->type == KNOT_RRTYPE_SOA && knot_dname_in_bailiwick(rr->owner, qry->zone_cut.name) > 0) { /* SOA below cut in authority indicates different authority, @@ -466,6 +478,9 @@ static int process_authority(knot_pkt_t *pkt, struct kr_request *req) if (glue_cnt) { VERBOSE_MSG("<= loaded %d glue addresses\n", glue_cnt); } + if (glue_cnt > GLUE_COUNT_THROTTLE) { + VERBOSE_MSG("<= (some may have been omitted due to being too many)\n"); + } if ((qry->flags.DNSSEC_WANT) && (result == KR_STATE_CONSUME)) { diff --git a/lib/zonecut.c b/lib/zonecut.c index 74db164a1..00b9145b2 100644 --- a/lib/zonecut.c +++ b/lib/zonecut.c @@ -282,6 +282,7 @@ int kr_zonecut_set_sbelt(struct kr_context *ctx, struct kr_zonecut *cut) /** Fetch address for zone cut. Any rank is accepted (i.e. glue as well). */ static addrset_info_t fetch_addr(pack_t *addrs, const knot_dname_t *ns, uint16_t rrtype, + int *addr_budget, knot_mm_t *mm_pool, const struct kr_query *qry) // LATER(optim.): excessive data copying { @@ -315,6 +316,12 @@ static addrset_info_t fetch_addr(pack_t *addrs, const knot_dname_t *ns, uint16_t return AI_UNKNOWN; } + *addr_budget -= cached_rr.rrs.count - 1; + if (*addr_budget < 0) { + cached_rr.rrs.count += *addr_budget; + *addr_budget = 0; + } + /* Reserve memory in *addrs. Implementation detail: * pack_t cares for lengths, so we don't store those in the data. */ const size_t pack_extra_size = cached_rr.rrs.size @@ -380,6 +387,22 @@ static int fetch_ns(struct kr_context *ctx, struct kr_zonecut *cut, return ret; } + /* Consider at most 13 first NSs (like root). It's a trivial approach + * to limit our resources when choosing NSs. Otherwise DoS might be viable. + * We're not aware of any reasonable use case for having many NSs. */ + if (ns_rds.count > 13) { + if (kr_log_is_debug_qry(ZCUT, qry)) { + auto_free char *name_txt = kr_dname_text(name); + VERBOSE_MSG(qry, "NS %s too large, reducing from %d names\n", + name_txt, (int)ns_rds.count); + } + ns_rds.count = 13; + } + /* Also trivially limit the total address count: + * first A and first AAAA are for free per NS, + * but the rest get a shared small limit and get skipped if exhausted. */ + int addr_budget = 8; + /* Insert name servers for this zone cut, addresses will be looked up * on-demand (either from cache or iteratively) */ bool all_bad = true; /**< All NSs (seen so far) are in a bad state. */ @@ -401,8 +424,10 @@ static int fetch_ns(struct kr_context *ctx, struct kr_zonecut *cut, addrset_info_t infos[2]; /* Fetch NS reputation and decide whether to prefetch A/AAAA records. */ - infos[0] = fetch_addr(*pack, ns_name, KNOT_RRTYPE_A, cut->pool, qry); - infos[1] = fetch_addr(*pack, ns_name, KNOT_RRTYPE_AAAA, cut->pool, qry); + infos[0] = fetch_addr(*pack, ns_name, KNOT_RRTYPE_A, &addr_budget, + cut->pool, qry); + infos[1] = fetch_addr(*pack, ns_name, KNOT_RRTYPE_AAAA, &addr_budget, + cut->pool, qry); #if 0 /* rather unlikely to be useful unless changing some zcut code */ if (kr_log_is_debug_qry(ZCUT, qry)) { @@ -448,6 +473,14 @@ static int fetch_ns(struct kr_context *ctx, struct kr_zonecut *cut, VERBOSE_MSG(qry, "cut %s: all NSs bad, count = %d\n", name_txt, (int)ns_rds.count); } + + kr_assert(addr_budget >= 0); + if (addr_budget <= 0 && kr_log_is_debug_qry(ZCUT, qry)) { + auto_free char *name_txt = kr_dname_text(name); + VERBOSE_MSG(qry, "NS %s have too many addresses together, reduced\n", + name_txt); + } + knot_rdataset_clear(&ns_rds, cut->pool); return all_bad ? ELOOP : kr_ok(); } diff --git a/lib/zonecut.h b/lib/zonecut.h index 56ca834d4..b44977ae4 100644 --- a/lib/zonecut.h +++ b/lib/zonecut.h @@ -140,6 +140,8 @@ int kr_zonecut_set_sbelt(struct kr_context *ctx, struct kr_zonecut *cut); /** * Populate zone cut address set from cache. * + * The size is limited to avoid possibility of doing too much CPU work. + * * @param ctx resolution context (to fetch data from LRU caches) * @param cut zone cut to be populated * @param name QNAME to start finding zone cut for