Hello list,

The --rebind-domain-ok option is broken in v2.86 and on master in the
following ways:

* In v2.85, --stop-dns-rebind --rebind-domain-ok=test.me would only allow
  "test.me" and subdomains of "test.me" to return private addresses to the
  user. A query for localtest.me, which is known to return 127.0.0.1, is
  blocked as expected.

  In v2.86, the --rebind-domain-ok feature is implemented with a simple suffix
  comparison, which means that --stop-dns-rebind --rebind-domain-ok=test.me
  fails to block the response of "127.0.0.1" for "localtest.me" because
  "test.me" is a suffix of "localtest.me".

  Here is a reproducible example:

    v2.85$ src/dnsmasq -C /dev/null -a 127.0.0.1 -p 5353 -S 1.1.1.1 -qd 
--no-resolv --stop-dns-rebind --rebind-domain-ok=test.me
    dnsmasq: started, version 2.85 cachesize 150
    dnsmasq: compile time options: IPv6 GNU-getopt no-DBus no-UBus no-i18n 
no-IDN DHCP DHCPv6 no-Lua TFTP no-conntrack ipset auth no-cryptohash no-DNSSEC 
loop-detect inotify dumpfile
    dnsmasq: using nameserver 1.1.1.1#53
    dnsmasq: read /etc/hosts - 1 addresses
    dnsmasq: query[A] localtest.me from 127.0.0.1
    dnsmasq: forwarded localtest.me to 1.1.1.1
    dnsmasq: possible DNS-rebind attack detected: localtest.me

    master$ src/dnsmasq -C /dev/null -a 127.0.0.1 -p 5353 -S 1.1.1.1 -qd 
--no-resolv --stop-dns-rebind --rebind-domain-ok=test.me
    dnsmasq: started, version 2.87test4-4-gc0409fa cachesize 150
    dnsmasq: compile time options: IPv6 GNU-getopt no-DBus no-UBus no-i18n 
no-IDN DHCP DHCPv6 no-Lua TFTP no-conntrack ipset no-nftset auth no-cryptohash 
no-DNSSEC loop-detect inotify dumpfile
    dnsmasq: using nameserver 1.1.1.1#53
    dnsmasq: read /etc/hosts - 1 addresses
    dnsmasq: query[A] localtest.me from 127.0.0.1
    dnsmasq: forwarded localtest.me to 1.1.1.1
    dnsmasq: reply localtest.me is 127.0.0.1

* In v2.85, --stop-dns-rebind --rebind-domain-ok=// means "stop potential DNS
  rebinding attacks, but allow private responses for dotless domains", which
  mirrors the special meaning of // in the --server option.

  In v2.86, --stop-dns-rebind --rebind-domain-ok=// crashes dnsmasq during
  resolution.

    v2.85$ src/dnsmasq -C /dev/null -a 127.0.0.1 -p 5353 -S 192.168.0.1 -qd 
--no-resolv --stop-dns-rebind --rebind-domain-ok=//
    dnsmasq: started, version 2.85 cachesize 150
    dnsmasq: compile time options: IPv6 GNU-getopt no-DBus no-UBus no-i18n 
no-IDN DHCP DHCPv6 no-Lua TFTP no-conntrack ipset auth no-cryptohash no-DNSSEC 
loop-detect inotify dumpfile
    dnsmasq: using nameserver 1.1.1.1#53
    dnsmasq: read /etc/hosts - 1 addresses
    dnsmasq: query[A] brother-laser-printer from 127.0.0.1
    dnsmasq: forwarded brother-laser-printer to 192.168.0.1
    dnsmasq: reply brother-laser-printer is 192.168.0.50

    master$ src/dnsmasq -C /dev/null -a 127.0.0.1 -p 5353 -S 192.168.0.1 -qd 
--no-resolv --stop-dns-rebind --rebind-domain-ok=//
    dnsmasq: started, version 2.87test4-2-g9560658 cachesize 150
    dnsmasq: compile time options: IPv6 GNU-getopt no-DBus no-UBus no-i18n 
no-IDN DHCP DHCPv6 no-Lua TFTP no-conntrack ipset no-nftset auth no-cryptohash 
no-DNSSEC loop-detect inotify dumpfile
    dnsmasq: using nameserver 1.1.1.1#53
    dnsmasq: read /etc/hosts - 1 addresses
    dnsmasq: query[A] brother-laser-printer from 127.0.0.1
    Segmentation fault (core dumped)

  Note that the new suffix-matching algorithm of --rebind-domain-ok means that
  even if the crash above is fixed, an empty option value effectively negates
  --stop-dns-rebind because the empty string is a suffix of all possible
  strings.

The attached patches address the issues above and restore the behavior of
--rebind-domain-ok back to the semantics of v2.85. The patches are also
available on Github:

https://github.com/guns/dnsmasq/compare/master...fix-option-rebind-domain-ok
https://github.com/guns/dnsmasq/commit/3abd86eb9e53efeea270767fd251284851d706d4
https://github.com/guns/dnsmasq/commit/4afb5b4ce50a4d3b7f917d2ce83ea1b27dd55db5
From 3abd86eb9e53efeea270767fd251284851d706d4 Mon Sep 17 00:00:00 2001
From: guns <s...@sungpae.com>
Date: Wed, 17 Nov 2021 14:15:35 -0600
Subject: [PATCH 1/2] Correctly return a heap-allocated empty string instead of
 NULL

Commit 32e15c3f458c2e8838a9ecf7d478ecb6750516bf added the following
change:

  --- a/src/option.c
  +++ b/src/option.c
  @@ -654,7 +654,7 @@ static char *canonicalise_opt(char *s)
       return 0;

     if (strlen(s) == 0)
  -    return "";
  +    return opt_string_alloc("");

     unhide_metas(s);
     if (!(ret = canonicalise(s, &nomem)) && nomem)

Unfortunately, opt_string_alloc(const char *cp) returns NULL when
strlen(cp) == 0, which in turn causes --rebind-domain-ok='' to crash
with SIGSEGV.
---
 src/option.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/option.c b/src/option.c
index ac2ba20..9e315a5 100644
--- a/src/option.c
+++ b/src/option.c
@@ -663,7 +663,7 @@ static char *canonicalise_opt(char *s)
     return 0;
 
   if (strlen(s) == 0)
-    return opt_string_alloc("");
+    return opt_malloc(1); /* Heap-allocated empty string */
 
   unhide_metas(s);
   if (!(ret = canonicalise(s, &nomem)) && nomem)
-- 
2.33.1

From 4afb5b4ce50a4d3b7f917d2ce83ea1b27dd55db5 Mon Sep 17 00:00:00 2001
From: guns <s...@sungpae.com>
Date: Thu, 18 Nov 2021 02:04:02 -0600
Subject: [PATCH 2/2] Fix --rebind-domain-ok domain matching

Commit 32e15c3f458c2e8838a9ecf7d478ecb6750516bf simplified the
implementation of the rebind-domain-ok feature, but unfortunately, it
also changed its semantics.

In v2.85, --stop-dns-rebind --rebind-domain-ok=test.me would only allow
"test.me" and subdomains of "test.me" to return private addresses to
the user. A query for localtest.me, which is known to return 127.0.0.1,
is blocked as expected.

In v2.86, the --rebind-domain-ok feature is implemented as a
simple suffix comparison, which means that --stop-dns-rebind
--rebind-domain-ok=test.me fails to block the response of "127.0.0.1"
for "localtest.me" because "test.me" is a suffix of "localtest.me".

The special meaning of the empty string is also affected by this bug. In
v2.85, --stop-dns-rebind --rebind-domain-ok=// was interpreted to mean
"stop potential DNS rebinding attacks, but allow responses for dotless
domains". This mirrored the special meaning of // in the --server
option.

In contrast, v2.86, --stop-dns-rebind --rebind-domain-ok=// effectively
negates --stop-dns-rebind because the empty string is a suffix of all
possible strings.

This patch restores the semantics of the --rebind-domain-ok option back
to v2.85.
---
 src/forward.c | 85 ++++++++++++++++++++++++++++++++++++++++++++++++---
 src/option.c  |  7 +++++
 2 files changed, 88 insertions(+), 4 deletions(-)

diff --git a/src/forward.c b/src/forward.c
index 04635b3..6b2cf6a 100644
--- a/src/forward.c
+++ b/src/forward.c
@@ -149,14 +149,91 @@ static void server_send_log(struct server *server, int fd,
 }
 #endif
 
+struct substring {
+  const char *base;
+  size_t offset;
+  size_t len;
+};
+
+static int substrings_are_equal(struct substring *a, struct substring *b)
+{
+  if (a->len == b->len)
+    return strncmp(a->base + a->offset, b->base + b->offset, a->len) == 0;
+  else
+    return 0;
+}
+
+static int rfind_next_domain_part(struct substring *ss)
+{
+  if (ss->offset == 0)
+    return 0; /* No more domain parts */
+
+  ss->len = 0;
+  ss->offset--;
+
+  if (ss->base[ss->offset] == '.')
+    {
+      if (ss->offset == 0)
+       return 0; /* Domain has a leading dot, so there are no more domain 
parts */
+      ss->offset--;
+    }
+
+  ss->len++;
+
+  while (ss->offset > 0)
+    {
+      if (ss->base[ss->offset - 1] == '.')
+       break;
+      ss->offset--;
+      ss->len++;
+    }
+
+  return 1;
+}
+
 static int domain_no_rebind(char *domain)
 {
+  if (daemon->no_rebind == NULL)
+    return 0;
+
   struct rebind_domain *rbd;
-  size_t tlen, dlen = strlen(domain);
-  
+  size_t dlen = strlen(domain);
+
   for (rbd = daemon->no_rebind; rbd; rbd = rbd->next)
-    if (dlen >= (tlen = strlen(rbd->domain)) && strcmp(rbd->domain, 
&domain[dlen - tlen]) == 0)
-      return 1;
+    {
+      struct substring domain_ss = {
+       .base = domain,
+       .offset = dlen,
+       .len = 0
+      };
+      struct substring rbd_ss = {
+       .base = rbd->domain,
+       .offset = strlen(rbd->domain),
+       .len = 0
+      };
+      int matches_rebind_domain = 1;
+
+      /* rebind-domain-ok=// means allow "dotless" domains */
+      if (rbd_ss.offset == 0)
+       {
+         if (strchr(domain, '.') == NULL)
+           return 1;
+         else
+           continue;
+       }
+
+      while (rfind_next_domain_part(&rbd_ss))
+       {
+         if (!rfind_next_domain_part(&domain_ss) || 
!substrings_are_equal(&domain_ss, &rbd_ss))
+           {
+             matches_rebind_domain = 0;
+             break;
+           }
+       }
+
+      if (matches_rebind_domain)
+       return 1;
+    }
 
   return 0;
 }
diff --git a/src/option.c b/src/option.c
index 9e315a5..4667d3d 100644
--- a/src/option.c
+++ b/src/option.c
@@ -2729,6 +2729,13 @@ static int one_opt(int option, char *arg, char *errstr, 
char *gen_err, int comma
          comma = split_chr(arg, '/');
          new = opt_malloc(sizeof(struct  rebind_domain));
          new->domain = canonicalise_opt(arg);
+         if (new->domain != 0)
+           {
+             /* Let "example.com." == "example.com" */
+             int dlen = strlen(new->domain);
+             if (dlen > 0 && new->domain[dlen - 1] == '.')
+               new->domain[dlen - 1] = 0;
+           }
          new->next = daemon->no_rebind;
          daemon->no_rebind = new;
          arg = comma;
-- 
2.33.1

Attachment: signature.asc
Description: PGP signature

_______________________________________________
Dnsmasq-discuss mailing list
Dnsmasq-discuss@lists.thekelleys.org.uk
https://lists.thekelleys.org.uk/cgi-bin/mailman/listinfo/dnsmasq-discuss

Reply via email to