Repository: c-ares/c-ares (2026-07-03T17:52:22Z)

Issue:
SRV record should not allow name compression
SUMMARY

c-ares accepts a DNS SRV record whose TARGET field is encoded as a
compression pointer and exposes it to applications as a fully resolved
typed SRV RR. RFC 2782 explicitly prohibits name compression in SRV
RDATA. c-ares's own ares_dns_rec_allow_name_comp() reflects this by
excluding SRV from its allowlist, but the SRV typed parser calls
ares_dns_name_parse() unconditionally, bypassing that intent entirely.

The issue is reachable from raw DNS response bytes and surfaces through
both the modern ares_dns_parse() API and the legacy
ares_parse_srv_reply() API.


VERSION TESTED

v1.34.6 (latest release)
commit 3ac47ee4ebc0373fe57677523ec960f6c1cb0e15

Confirmed present in:
  src/lib/record/ares_dns_parse.c
  src/lib/record/ares_dns_mapping.c


ROOT CAUSE

ares_dns_rec_allow_name_comp() in ares_dns_mapping.c correctly
enumerates only RFC 1035-era record types as compression-allowed (NS,
CNAME, SOA, PTR, HINFO, MX, TXT). SRV is intentionally absent.

However, the SRV typed parser (ares_dns_parse_rr_srv) calls
ares_dns_parse_and_set_dns_name() for the TARGET field, which
unconditionally calls ares_dns_name_parse(). That function always
follows compression pointers regardless of record type.

ares_dns_rec_allow_name_comp() is consulted at line ~1143 of
ares_dns_parse.c only to decide raw-RR fallback flags. It is never
used to gate whether ares_dns_name_parse() follows compression pointers
inside the per-type parsers. The allowlist exists and is correct in
intent; it is simply never enforced at the name-parsing level for
typed parsers.

RFC 2782 states the following for the TARGET field:

  "Unless and until permitted by future standards action, name
   compression is not to be used for this field."


IMPACT

A network-based attacker who can supply a crafted DNS response (via a
rogue DNS server, DNS spoofing, or a poisoned cache) can deliver a
malformed SRV record whose TARGET is a compression pointer. c-ares
accepts this record as a valid typed SRV RR and exposes the resolved
name to the calling application with no indication that the wire
encoding was non-compliant.

Applications that trust the parsed SRV TARGET receive data derived from
a response that should have been rejected or returned as a raw RR. The
issue affects any application using c-ares for SRV resolution.

Note: This is a standards-compliance and parser validation issue.

Reported By: hextheshadow0x@gmail.com




Discussion:

Patch:
diff --git a/src/lib/ares_private.h b/src/lib/ares_private.h
index 3dbdf7a177..fc9032af7a 100644
--- a/src/lib/ares_private.h
+++ b/src/lib/ares_private.h
@@ -537,10 +537,15 @@ void ares_gethostbyaddr_nolock(ares_channel_t *channel, const void *addr,
  *                         ares_free()'d by the caller.
  *  \param[in] is_hostname if ARES_TRUE, will validate the character set for
  *                         a valid hostname or will return error.
+ *  \param[in] allow_compression if ARES_FALSE, a compression pointer
+ *                         encountered while parsing the name will be rejected
+ *                         with ARES_EBADNAME.  Used for RR types (e.g. SRV)
+ *                         whose RDATA names must not use name compression.
  *  \return ARES_SUCCESS on success
  */
 ares_status_t ares_dns_name_parse(ares_buf_t *buf, char **name,
-                                  ares_bool_t is_hostname);
+                                  ares_bool_t is_hostname,
+                                  ares_bool_t allow_compression);
 
 /*! Write the DNS name to the buffer in the DNS domain-name syntax as a
  *  series of labels.  The maximum domain name length is 255 characters with
diff --git a/src/lib/legacy/ares_expand_name.c b/src/lib/legacy/ares_expand_name.c
index 72668f4cb6..ee1d70076b 100644
--- a/src/lib/legacy/ares_expand_name.c
+++ b/src/lib/legacy/ares_expand_name.c
@@ -69,7 +69,7 @@ ares_status_t ares_expand_name_validated(const unsigned char *encoded,
   }
 
   start_len = ares_buf_len(buf);
-  status    = ares_dns_name_parse(buf, s, is_hostname);
+  status    = ares_dns_name_parse(buf, s, is_hostname, ARES_TRUE);
   if (status != ARES_SUCCESS) {
     goto done;
   }
diff --git a/src/lib/record/ares_dns_name.c b/src/lib/record/ares_dns_name.c
index 134f3edccf..5bba8bb7c3 100644
--- a/src/lib/record/ares_dns_name.c
+++ b/src/lib/record/ares_dns_name.c
@@ -549,7 +549,8 @@ static ares_status_t ares_fetch_dnsname_into_buf(ares_buf_t *buf,
 }
 
 ares_status_t ares_dns_name_parse(ares_buf_t *buf, char **name,
-                                  ares_bool_t is_hostname)
+                                  ares_bool_t is_hostname,
+                                  ares_bool_t allow_compression)
 {
   size_t        save_offset = 0;
   unsigned char c;
@@ -608,6 +609,13 @@ ares_status_t ares_dns_name_parse(ares_buf_t *buf, char **name,
        */
       size_t offset = (size_t)((c & 0x3F) << 8);
 
+      /* Some RR types (e.g. SRV) do not permit name compression within their
+       * RDATA.  Reject a compression pointer when it is not allowed. */
+      if (!allow_compression) {
+        status = ARES_EBADNAME;
+        goto fail;
+      }
+
       /* Fetch second byte of the redirect length */
       status = ares_buf_fetch_bytes(buf, &c, 1);
       if (status != ARES_SUCCESS) {
diff --git a/src/lib/record/ares_dns_parse.c b/src/lib/record/ares_dns_parse.c
index 113438ffb3..e13555692c 100644
--- a/src/lib/record/ares_dns_parse.c
+++ b/src/lib/record/ares_dns_parse.c
@@ -46,8 +46,14 @@ static ares_status_t ares_dns_parse_and_set_dns_name(ares_buf_t    *buf,
 {
   ares_status_t status;
   char         *name = NULL;
+  /* Only RR types defined in RFC1035 may use name compression within their
+   * RDATA (RFC3597).  Reject compression pointers for any other type (e.g.
+   * SRV per RFC2782) to match the write-side policy and avoid following
+   * pointers that a non-understanding nameserver could not have rewritten. */
+  ares_bool_t   allow_compression =
+    ares_dns_rec_allow_name_comp(ares_dns_rr_get_type(rr));
 
-  status = ares_dns_name_parse(buf, &name, is_hostname);
+  status = ares_dns_name_parse(buf, &name, is_hostname, allow_compression);
   if (status != ARES_SUCCESS) {
     return status;
   }
@@ -1399,7 +1405,7 @@ static ares_status_t ares_dns_parse_qd(ares_buf_t        *buf,
    */
 
   /* Name */
-  status = ares_dns_name_parse(buf, &name, ARES_FALSE);
+  status = ares_dns_name_parse(buf, &name, ARES_FALSE, ARES_TRUE);
   if (status != ARES_SUCCESS) {
     goto done;
   }
@@ -1470,7 +1476,7 @@ static ares_status_t ares_dns_parse_rr(ares_buf_t *buf, unsigned int flags,
    */
 
   /* Name */
-  status = ares_dns_name_parse(buf, &name, ARES_FALSE);
+  status = ares_dns_name_parse(buf, &name, ARES_FALSE, ARES_TRUE);
   if (status != ARES_SUCCESS) {
     goto done;
   }


Test patch:
diff --git a/test/ares-test-parse-srv.cc b/test/ares-test-parse-srv.cc
index 6956bebe46..2bb2e30b4a 100644
--- a/test/ares-test-parse-srv.cc
+++ b/test/ares-test-parse-srv.cc
@@ -312,5 +312,38 @@ TEST_F(LibraryTest, ParseSrvReplyAllocFail) {
   }
 }
 
+// RFC 2782: the SRV TARGET must not use name compression.  A response whose
+// SRV target is a compression pointer must be rejected rather than followed.
+TEST_F(LibraryTest, ParseSrvRejectsCompressedTarget) {
+  const unsigned char data[] = {
+    0x12, 0x34,              // qid
+    0x84, 0x00,              // response + AA, rcode NOERROR
+    0x00, 0x01,              // qdcount
+    0x00, 0x01,              // ancount
+    0x00, 0x00,              // nscount
+    0x00, 0x00,              // arcount
+    // Question (name starts at offset 0x0c)
+    0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e',
+    0x03, 'c', 'o', 'm',
+    0x00,
+    0x00, 0x21,              // type SRV (33)
+    0x00, 0x01,              // class IN
+    // Answer
+    0xc0, 0x0c,              // name -> pointer to example.com
+    0x00, 0x21,              // type SRV
+    0x00, 0x01,              // class IN
+    0x00, 0x00, 0x00, 0x3c,  // TTL
+    0x00, 0x08,              // rdlength
+    0x00, 0x0a,              // priority
+    0x00, 0x14,              // weight
+    0x00, 0x1e,              // port
+    0xc0, 0x0c,              // TARGET -> compression pointer (illegal for SRV)
+  };
+
+  ares_dns_record_t *dnsrec = NULL;
+  EXPECT_EQ(ARES_EBADNAME, ares_dns_parse(data, sizeof(data), 0, &dnsrec));
+  EXPECT_EQ(nullptr, dnsrec);
+}
+
 }  // namespace test
 }  // namespace ares
diff --git a/test/ares-test-parse.cc b/test/ares-test-parse.cc
index b00fe62f0c..0a20245dda 100644
--- a/test/ares-test-parse.cc
+++ b/test/ares-test-parse.cc
@@ -423,5 +423,37 @@ TEST_F(LibraryTest, ParseMultipleOptRejected) {
   EXPECT_EQ(nullptr, dnsrec);
 }
 
+// RFC 3597: only RR types defined in RFC 1035 may use name compression within
+// their RDATA.  A modern type such as SVCB must reject a compressed target,
+// same as SRV -- this exercises the shared policy, not an SRV special case.
+TEST_F(LibraryTest, ParseSvcbRejectsCompressedTarget) {
+  const unsigned char data[] = {
+    0x12, 0x34,              // qid
+    0x84, 0x00,              // response + AA
+    0x00, 0x01,              // qdcount
+    0x00, 0x01,              // ancount
+    0x00, 0x00,              // nscount
+    0x00, 0x00,              // arcount
+    // Question (name at offset 0x0c)
+    0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e',
+    0x03, 'c', 'o', 'm',
+    0x00,
+    0x00, 0x40,              // type SVCB (64)
+    0x00, 0x01,              // class IN
+    // Answer
+    0xc0, 0x0c,              // name -> pointer to example.com
+    0x00, 0x40,              // type SVCB
+    0x00, 0x01,              // class IN
+    0x00, 0x00, 0x00, 0x3c,  // TTL
+    0x00, 0x04,              // rdlength
+    0x00, 0x01,              // SvcPriority
+    0xc0, 0x0c,              // TargetName -> compression pointer (illegal)
+  };
+
+  ares_dns_record_t *dnsrec = NULL;
+  EXPECT_EQ(ARES_EBADNAME, ares_dns_parse(data, sizeof(data), 0, &dnsrec));
+  EXPECT_EQ(nullptr, dnsrec);
+}
+
 }  // namespace test
 }  // namespace ares