Skip to content
← All writing

My DMARC check passed domains that had no protection at all

  • email
  • 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. -all is a hard fail, ~all is a soft fail, +all is 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, quarantine and reject scored 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.