Browse Source

fix(api): validate MX, NS, SRV for hostname content

Peter Thomassen 4 years ago
parent
commit
160a1dda11
4 changed files with 45 additions and 10 deletions
  1. 33 2
      api/desecapi/dns.py
  2. 4 1
      api/desecapi/models.py
  3. 4 3
      api/desecapi/tests/test_rrsets.py
  4. 4 4
      test/e2e2/spec/test_api_rr.py

+ 33 - 2
api/desecapi/dns.py

@@ -1,11 +1,13 @@
+import re
 import struct
 
 from ipaddress import IPv6Address
 
 import dns
+import dns.name
 import dns.rdtypes.txtbase, dns.rdtypes.svcbbase
-import dns.rdtypes.ANY.CDS, dns.rdtypes.ANY.DLV, dns.rdtypes.ANY.DS
-import dns.rdtypes.IN.AAAA
+import dns.rdtypes.ANY.CDS, dns.rdtypes.ANY.DLV, dns.rdtypes.ANY.DS, dns.rdtypes.ANY.MX, dns.rdtypes.ANY.NS
+import dns.rdtypes.IN.AAAA, dns.rdtypes.IN.SRV
 
 
 def _strip_quotes_decorator(func):
@@ -102,3 +104,32 @@ class DLV(_DigestLengthMixin, dns.rdtypes.ANY.DLV.DLV):
 @dns.immutable.immutable
 class DS(_DigestLengthMixin, dns.rdtypes.ANY.DS.DS):
     pass
+
+
+def _HostnameMixin(name_field, *, allow_root):
+    # Taken from https://github.com/PowerDNS/pdns/blob/4646277d05f293777a3d2423a3b188ccdf42c6bc/pdns/dnsname.cc#L419
+    hostname_re = re.compile(r'^(([A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)\.)+$')
+
+    class Mixin:
+        def to_text(self, origin=None, relativize=True, **kw):
+            name = getattr(self, name_field)
+            if not (allow_root and name == dns.name.root) and hostname_re.match(str(name)) is None:
+                raise ValueError(f'invalid {name_field}: {name}')
+            return super().to_text(origin, relativize, **kw)
+
+    return Mixin
+
+
+@dns.immutable.immutable
+class MX(_HostnameMixin('exchange', allow_root=True), dns.rdtypes.ANY.MX.MX):
+    pass
+
+
+@dns.immutable.immutable
+class NS(_HostnameMixin('target', allow_root=False), dns.rdtypes.ANY.NS.NS):
+    pass
+
+
+@dns.immutable.immutable
+class SRV(_HostnameMixin('target', allow_root=True), dns.rdtypes.IN.SRV.SRV):
+    pass

+ 4 - 1
api/desecapi/models.py

@@ -40,7 +40,7 @@ from rest_framework.exceptions import APIException
 
 from desecapi import metrics
 from desecapi import pdns
-from desecapi.dns import AAAA, CDS, DLV, DS, LongQuotedTXT
+from desecapi.dns import AAAA, CDS, DLV, DS, LongQuotedTXT, MX, NS, SRV
 
 logger = logging.getLogger(__name__)
 psl = psl_dns.PSL(resolver=settings.PSL_RESOLVER, timeout=.5)
@@ -685,6 +685,9 @@ class RR(ExportModelOperationsMixin('RR'), models.Model):
         dns.rdatatype.CDS: CDS,  # TODO remove when https://github.com/rthalley/dnspython/pull/625 is in main codebase
         dns.rdatatype.DLV: DLV,  # TODO remove when https://github.com/rthalley/dnspython/pull/625 is in main codebase
         dns.rdatatype.DS: DS,  # TODO remove when https://github.com/rthalley/dnspython/pull/625 is in main codebase
+        dns.rdatatype.MX: MX,  # do DNS name validation the same way as pdns
+        dns.rdatatype.NS: NS,  # do DNS name validation the same way as pdns
+        dns.rdatatype.SRV: SRV,  # do DNS name validation the same way as pdns
         dns.rdatatype.TXT: LongQuotedTXT,  # we slightly deviate from RFC 1035 and allow tokens longer than 255 bytes
         dns.rdatatype.SPF: LongQuotedTXT,  # we slightly deviate from RFC 1035 and allow tokens longer than 255 bytes
     }

+ 4 - 3
api/desecapi/tests/test_rrsets.py

@@ -393,6 +393,7 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
                      '23 12 59.000 N 42 22 48.500 W 65.00m 20.00m 10.00m 10.00m')),
             ('MX', ('10 010.1.1.1.', '10 010.1.1.1.')),
             ('MX', ('010 010.1.1.2.', '10 010.1.1.2.')),
+            ('MX', ('0 .', '0 .')),
             ('NAPTR', ('100  50  "s"  "z3950+I2L+I2C"     ""  _z3950._tcp.gatech.edu.',
                        '100 50 "s" "z3950+I2L+I2C" "" _z3950._tcp.gatech.edu.')),
             ('NS', ('EXaMPLE.COM.', 'example.com.')),
@@ -622,17 +623,17 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
             # 'IPSECKEY': [],
             'KX': ['-1 example.com', '10 example.com'],
             'LOC': ['23 12 61.000 N 42 22 48.500 W 65.00m 20.00m 10.00m 10.00m', 'foo', '1.1.1.1'],
-            'MX': ['10 example.com', 'example.com.', '-5 asdf.', '65537 asdf.'],
+            'MX': ['10 example.com', 'example.com.', '-5 asdf.', '65537 asdf.', '10 _foo.example.com.', '10 $url.'],
             'NAPTR': ['100  50  "s"  "z3950+I2L+I2C"     ""  _z3950._tcp.gatech.edu',
                       '100  50  "s"     ""  _z3950._tcp.gatech.edu.',
                       '100  50  3 2  "z3950+I2L+I2C"     ""  _z3950._tcp.gatech.edu.'],
-            'NS': ['ns1.example.com', '127.0.0.1'],
+            'NS': ['ns1.example.com', '127.0.0.1', '_foobar.example.dedyn.io.', '.'],
             'OPENPGPKEY': ['1 2 3'],
             'PTR': ['"example.com."', '10 *.example.com.'],
             'RP': ['hostmaster.example.com.', '10 foo.'],
             'SMIMEA': ['3 1 0 aGVsbG8gd29ybGQh'],
             'SPF': ['"v=spf1', 'v=spf1 include:example.com ~all'],
-            'SRV': ['0 0 0 0', '100 5061 example.com.'],
+            'SRV': ['0 0 0 0', '100 5061 example.com.', '0 0 16920 _foo.example.com.', '0 0 16920 $url.'],
             'SSHFP': ['aabbcceeddff'],
             'SVCB': [
                 '0 svc4-baz.example.net. keys=val',

+ 4 - 4
test/e2e2/spec/test_api_rr.py

@@ -54,7 +54,7 @@ VALID_RECORDS_CANONICAL = {
     'HINFO': ['"ARMv8-A" "Linux"'],
     'HTTPS': ['1 h3POOL.exaMPLe. alpn=h2,h3 echconfig="MTIzLi4uCg=="'],
     # 'IPSECKEY': ['12 0 2 . asdfdf==', '03 1 1 127.0.00.1 asdfdf==', '12 3 1 example.com. asdfdf==',],
-    'KX': ['4 example.com.', '28 io.'],
+    'KX': ['4 example.com.', '28 io.', '0 .'],
     'LOC': [
         '23 12 59.000 N 42 22 48.500 W 65.00m 20.00m 10.00m 10.00m',
     ],
@@ -263,7 +263,7 @@ VALID_RECORDS_NON_CANONICAL = {
     ],
     'TLSA': ['003 00 002 696B8F6B92A913560b23ef5720c378881faffe74432d04eb35db957c0a93987b47adf26abb5dac10ba482597ae16edb069b511bec3e26010d1927bf6392760dd',],
     'TXT': [
-        f'"{"a" * 498}"',
+        f'"{"a" * 498}" ',
         '"' + 124 * '🧥' + '==="',  # 501 byte total length
         '"🧥 👚 👕 👖 👔 👗 👙 👘 👠 👡 👢 👞 👟 🥾 🥿  🧦 🧤 🧣 🎩 🧢 👒 🎓 ⛑ 👑 👝 👛 👜 💼 🎒 "',
         '"🧥 👚 👕 👖 👔 👗 👙 👘 👠 👡 👢 👞 👟 🥾 🥿  🧦 🧤 🧣 🎩 🧢 👒 🎓 ⛑ 👑 👝 👛 👜 💼 🎒 👓 🕶 🥽 🥼 🌂 🧵"',
@@ -313,7 +313,7 @@ INVALID_RECORDS = {
     # 'IPSECKEY': [],
     'KX': ['-1 example.com', '10 example.com'],
     'LOC': ['23 12 61.000 N 42 22 48.500 W 65.00m 20.00m 10.00m 10.00m', 'foo', '1.1.1.1'],
-    'MX': ['10 example.com', 'example.com.', '-5 asdf.', '65537 asdf.'],
+    'MX': ['10 example.com', 'example.com.', '-5 asdf.', '65537 asdf.' '10 _foo.example.com.', '10 $url.'],
     'NAPTR': ['100  50  "s"  "z3950+I2L+I2C"     ""  _z3950._tcp.gatech.edu',
               '100  50  "s"     ""  _z3950._tcp.gatech.edu.',
               '100  50  3 2  "z3950+I2L+I2C"     ""  _z3950._tcp.gatech.edu.'],
@@ -323,7 +323,7 @@ INVALID_RECORDS = {
     'RP': ['hostmaster.example.com.', '10 foo.'],
     'SMIMEA': ['3 1 0 aGVsbG8gd29ybGQh', 'x 0 0 aabbccddeeff'],
     'SPF': ['"v=spf1', 'v=spf1 include:example.com ~all'],
-    'SRV': ['0 0 0 0', '100 5061 example.com.'],
+    'SRV': ['0 0 0 0', '100 5061 example.com.', '0 0 16920 _foo.example.com.', '0 0 16920 $url.'],
     'SSHFP': ['aabbcceeddff'],
     'SVCB': [
         '0 svc4-baz.example.net. keys=val',