This issue tracks the fix for issues #206 and #221.
Root cause
dmarcf_parse_received_spf() parses Received-SPF headers as semicolon-separated key=value pairs. It treated every = as a switch from key-collection to value-collection mode, even when already collecting a value or inside a quoted string.
VERP addresses embed the recipient in the local part using = as a separator (e.g. bounces+nonce=recipient@sender.example.com). When the parser hit that =, it reset its value buffer and started overwriting from the beginning. If the pre-= string was longer than the post-= string, leftover bytes corrupted the extracted domain — e.g. sender.com became sender.comnts. The domain comparison then failed and a valid SPF pass was discarded as neutral, producing a false DMARC fail.
Issue #221 (Twitter VERP with triple ===) accidentally produced the correct result because multiple resets walked the parser to the correct final fragment. Variants where the pre-last-= string is longer expose the same corruption.
Fix
- Track whether value-collection has started (
in_value flag) so = is only treated as a mode-switch once per key=value pair.
- Honour the
quoting flag for both = and ; to handle quoted envelope-from values correctly.
- Extracted the function into
opendmarc/opendmarc-spf-parse.c so it can be unit tested without depending on libmilter headers.
Regression tests cover the #206 case, the #221 case, and variants that expose the corruption in cases where #221's accidental correctness does not apply.
Closes #206. Related: #221.
This issue tracks the fix for issues #206 and #221.
Root cause
dmarcf_parse_received_spf()parsesReceived-SPFheaders as semicolon-separatedkey=valuepairs. It treated every=as a switch from key-collection to value-collection mode, even when already collecting a value or inside a quoted string.VERP addresses embed the recipient in the local part using
=as a separator (e.g.bounces+nonce=recipient@sender.example.com). When the parser hit that=, it reset its value buffer and started overwriting from the beginning. If the pre-=string was longer than the post-=string, leftover bytes corrupted the extracted domain — e.g.sender.combecamesender.comnts. The domain comparison then failed and a valid SPF pass was discarded as neutral, producing a false DMARC fail.Issue #221 (Twitter VERP with triple
===) accidentally produced the correct result because multiple resets walked the parser to the correct final fragment. Variants where the pre-last-=string is longer expose the same corruption.Fix
in_valueflag) so=is only treated as a mode-switch once per key=value pair.quotingflag for both=and;to handle quotedenvelope-fromvalues correctly.opendmarc/opendmarc-spf-parse.cso it can be unit tested without depending on libmilter headers.Regression tests cover the #206 case, the #221 case, and variants that expose the corruption in cases where #221's accidental correctness does not apply.
Closes #206. Related: #221.