]> git.ipfire.org Git - thirdparty/knot-resolver.git/commitdiff
lib/zonecut + iterator: limit large NS sets
authorVladimír Čunát <vladimir.cunat@nic.cz>
Wed, 17 Aug 2022 14:34:06 +0000 (16:34 +0200)
committerVladimír Čunát <vladimir.cunat@nic.cz>
Fri, 16 Sep 2022 11:29:15 +0000 (13:29 +0200)
It's a mitigation for CVE-2022-40188 and similar DoS attempts.
It's using really trivial approaches, at least for now.

NEWS
lib/layer/iterate.c
lib/zonecut.c
lib/zonecut.h

diff --git a/NEWS b/NEWS
index 56a4263596c3995462d13d2674a944089c6633d1..4add63db52b933a5f7235e1a2661a7678b8aa130 100644 (file)
--- 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)
index b1c3df40f01625f4b94d3b329622d573c6ee91e9..a730899325f68f06b5f07137b1dfdd2232c6f33a 100644 (file)
@@ -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)) {
index 74db164a19b0e874941eac75acbb0524409d07bd..00b9145b216fcf7de99f5415cca203cc23941f60 100644 (file)
@@ -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();
 }
index 56ca834d41fcfd8d8eb56578cdd3ed6c18f59b55..b44977ae471a7a0b9278c77905eb06eafe6f7f74 100644 (file)
@@ -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