~phil.pennock/mailman/dmarc-reject

« back to all changes in this revision

Viewing changes to Mailman/Utils.py

  • Committer: Phil Pennock
  • Date: 2013-03-18 22:07:57 UTC
  • Revision ID: dms@phil.spodhuis.org-20130318220757-rehs5aonbohkd5kg
Handle CNAMEs when chasing DMARC TXT records.

Handle TXT records missing tags, check all such records, etc.  Use \b
boundary anchors in regexp check.

(Should only be one, but if there are multiple, check them all, reject if
any of them say p=reject).

Show diffs side-by-side

added added

removed removed

Lines of Context:
35
35
import base64
36
36
import random
37
37
import urlparse
 
38
import collections
38
39
import htmlentitydefs
39
40
import email.Header
40
41
import email.Iterators
1081
1082
        resolver.timeout = 1
1082
1083
        resolver.lifetime = 5
1083
1084
        txt_recs = resolver.query(dmarc_domain, dns.rdatatype.TXT)
1084
 
    except dns.resolver.NXDOMAIN:
 
1085
    except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
1085
1086
        return False
1086
1087
    except DNSException, e:
1087
1088
        syslog('error', 'DNSException: Unable to query DMARC policy for %s (%s). %s',
1088
1089
              email, dmarc_domain, e.__class__)
1089
1090
        return False
1090
1091
    else:
 
1092
# people are already being dumb, don't trust them to provide honest DNS
 
1093
# where the answer section only contains what was asked for, nor to include
 
1094
# CNAMEs before the values they point to.
 
1095
        full_record = ""
 
1096
        results_by_name = collections.defaultdict(list)
 
1097
        cnames = {}
 
1098
        want_names = set([dmarc_domain + '.'])
1091
1099
        for txt_rec in txt_recs.response.answer:
1092
 
            assert( txt_rec.rdtype == dns.rdatatype.TXT)
1093
 
            if re.search(r"[^s]p=reject", "".join(txt_rec.items[0].strings), re.IGNORECASE):
1094
 
               return True
1095
 
    
 
1100
            if txt_rec.rdtype == dns.rdatatype.CNAME:
 
1101
                cnames[txt_rec.name.to_text()] = txt_rec.items[0].target.to_text()
 
1102
            if txt_rec.rdtype != dns.rdatatype.TXT:
 
1103
                continue
 
1104
            results_by_name[txt_rec.name.to_text()].append("".join(txt_rec.items[0].strings))
 
1105
        expands = list(want_names)
 
1106
        seen = set(expands)
 
1107
        while expands:
 
1108
            item = expands.pop(0)
 
1109
            if item in cnames:
 
1110
                if cnames[item] in seen:
 
1111
                    continue # cname loop
 
1112
                expands.append(cnames[item])
 
1113
                seen.add(cnames[item])
 
1114
                want_names.add(cnames[item])
 
1115
                want_names.discard(item)
 
1116
 
 
1117
        if len(want_names) != 1:
 
1118
            syslog('error', 'multiple DMARC entries in results for %s, processing each to be strict',
 
1119
                    dmarc_domain)
 
1120
        for name in want_names:
 
1121
            if name not in results_by_name:
 
1122
                continue
 
1123
            dmarcs = filter(lambda n: n.startswith('v=DMARC1;'), results_by_name[name])
 
1124
            if len(dmarcs) == 0:
 
1125
                return False
 
1126
            if len(dmarcs) > 1:
 
1127
                syslog('error', 'RRset of TXT records for %s has %d v=DMARC1 entries; testing them all',
 
1128
                        dmarc_domain, len(dmarc))
 
1129
            for entry in dmarcs:
 
1130
                if re.search(r'\bp=reject\b', entry, re.IGNORECASE):
 
1131
                    syslog('info', 'DMARC lookup for %s (%s) found p=reject in %s = %s',
 
1132
                            email, dmarc_domain, name, entry)
 
1133
                    return True
 
1134
 
1096
1135
    return False
1097
1136
 
1098
1137