My DMARC check passed domains that had no protection at all
- dns
- security
- false-positives
Email authentication is three DNS records. SPF says which servers may send as you. DKIM signs the message. DMARC says what a receiving server should do when the first two fail, and where to send the reports.
My auditing tool checked all three and it checked them badly.
The check I had written
It fetched the TXT records and looked for the strings. Is there an SPF record? Is there a DMARC record? Yes and yes, so, tick. Green. Next domain.
That is a check on whether somebody has heard of DMARC. It is not a check on whether they’re protected, and the difference is not academic.
Two ways to pass while being wide open
v=spf1 +all is a valid SPF record. It parses, it’s syntactically correct, and it says: any server on the internet may send email claiming to be this domain. It’s the opposite of what SPF is for. My check saw an SPF record and approved it.
p=none is a valid DMARC policy. It means monitor only. Take no action, deliver the mail anyway, just tell me about it if a reporting address is configured. It’s the correct place to start, because you turn it on, watch for a few weeks to find out which of your own legitimate senders you’d break, and then tighten to quarantine and eventually reject.
The problem is that p=none is also where most domains stop. It’s on the list, someone ticked it, and nothing about it is enforcing anything. Worse, p=none with no rua= address isn’t even monitoring, because there’s nowhere for the reports to go. That’s a record that does nothing whatsoever, and my tool called it a pass.
What it does now
I pulled the substring matching out and wrote something that parses the records properly. It’s in email_auth.py, and it now cares about:
- The SPF qualifier on the final mechanism.
-allis a hard fail,~allis a soft fail,+allis everybody. Only the first two count for much. - SPF DNS lookups against the limit of 10. Go over and conforming receivers give up evaluating, so a technically-correct record stops being applied.
- The DMARC policy value itself, with
none,quarantineandrejectscored differently rather than all treated as present. - Whether
rua=exists at all, because a monitor-only policy with nobody reading the reports is theatre. - The subdomain policy,
sp=, which is how a locked-down parent domain ends up with a wide open child nobody thought about.
The thing I keep chewing on
I ran it against domains I’m responsible for, and the results were worse than the old check had been telling me for months. Which is the point, and it still stung.
A check that returns a pass when the answer is no is worse than having no check, because it costs you the attention you’d otherwise have spent looking. I’d been walking past those domains for a year with a green tick in front of them.
So now when I write a check I try to ask what the most common wrong configuration actually looks like in the wild, and then make sure my check fails it. Not the absent case. The present-but-useless case, which is far more common, because somebody did the first 80% and then the ticket got closed.
There’s a version of this bug in almost every scanner I’ve looked at since. Presence is easy to test. Correctness is the job.