[Dnsmasq-discuss] [PATCH] Fix segfault and regressions in option --rebind-domain-ok
Sung Pae
self at sungpae.com
Thu Nov 18 18:21:26 UTC 2021
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
-------------- next part --------------
From 3abd86eb9e53efeea270767fd251284851d706d4 Mon Sep 17 00:00:00 2001
From: guns <self at 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
-------------- next part --------------
From 4afb5b4ce50a4d3b7f917d2ce83ea1b27dd55db5 Mon Sep 17 00:00:00 2001
From: guns <self at 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
-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 833 bytes
Desc: not available
URL: <http://lists.thekelleys.org.uk/pipermail/dnsmasq-discuss/attachments/20211118/11235501/attachment.sig>
More information about the Dnsmasq-discuss
mailing list