Vulnerability analysis · DMARC
On OpenDMARC, pct=4294967296 wraps to 0 and passes the range check, producing an enforcing policy. On Rspamd, pct=-1 becomes 0 and enforcement stops.
| Software | OpenDMARC and Rspamd (two distinct defects on the same tag) |
|---|---|
| Vendor | The Trusted Domain Project and the Rspamd project (Vsevolod Stakhov) |
| Source | https://github.com/trusteddomainproject/OpenDMARC |
| Affected | OpenDMARC 1.4.2 (32-bit unsigned wrap) and Rspamd 4.1.2, originally isolated on 3.10.2 (negative clamp). Newest published release checked on 2026-07-30. |
| Where | DMARC pct tag parsing and range check |
| Weakness | CWE-190 (Integer Overflow or Wraparound) on OpenDMARC; CWE-20 (Improper Input Validation) on Rspamd |
| Reachable by | Remote. The attacker must be able to influence the victim's published DMARC record; alternatively an administrator's typo produces the same effect. |
| CVSS v3.1 | CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:N (5.9 (Medium)) |
| Verification | Reproduced on 2026-07-28 against the current release (run 20260728-120957) with a scripted reproducer, negative controls, and a second independent implementation as the differential oracle. Nine of nine boundary probes deterministic across three repetitions; the modulo-2^32 model fits every observation. |
RFC 7489 section 6.3 says pct is an integer from 0 to 100. Values outside that range should be rejected. Two implementations mishandle them, in opposite directions.
OpenDMARC converts pct into a 32-bit unsigned integer first and range-checks the result afterward. The record is therefore accepted exactly when pct mod 2^32 lands in [0, 100]. A published value of 2^32 reduces to 0 and passes the check, which turns an invalid record into an enforcing policy. A value of 2^32+50 quietly becomes 50 percent sampling.
Rspamd does not wrap. It clamps a negative pct to 0 instead, and 0 means no enforcement.
Nine boundary probes were run against OpenDMARC:
pct= | OpenDMARC | Effect |
|---|---|---|
2147483648 (2^31) | permerror | p=reject not applied, fail-open |
4294967295 (2^32−1) | permerror | fail-open |
| 2^64 | permerror | fail-open |
4294967296 (2^32) | pass, p=reject applied | wraps to 0 and re-validates into an enforcing policy |
| 2^32+50 | pass (pct=50) | a garbage value silently becomes 50 percent sampling |
| 2^32+100 | pass (pct=100) | |
| 2^32+101 | permerror (101) |
One model explains all nine: accept if and only if pct mod 2^32 lies in [0, 100]. That is the signature of a range check applied after the conversion rather than before it.
Rspamd behaves differently on the same inputs. Values of 2^31, 2^32−1 and 2^64 all yield full DMARC_POLICY_REJECT on forged mail, so nothing wraps there. The defect on Rspamd is on the other side of zero. pct=-1, pct=-100 and pct=-2^31 are all clamped to 0, which switches enforcement off.
# OpenDMARC: an out-of-range value that becomes an enforcing policy with different sampling
_dmarc.victim.example. IN TXT "v=DMARC1; p=reject; pct=4294967296"
# Rspamd: a negative value that disables enforcement
_dmarc.victim.example. IN TXT "v=DMARC1; p=reject; pct=-1"
From: ceo@victim.example.Expected in both cases: the pct value is outside 0 to 100, so the record is invalid and the value must not be used.
Observed on OpenDMARC: 4294967296 mod 2^32 = 0, the record validates, and p=reject is applied with a sampling rate that is not the published one.
Observed on Rspamd: pct=-1 is clamped to 0 and nothing is enforced.
On OpenDMARC, any pct in the band [2^32, 2^32+100] turns into a valid enforcing policy whose sampling rate differs from what the record says. No error reaches the operator. The band at or above 2^31 that does not wrap back into range gives permerror instead, which drops p=reject entirely.
On Rspamd, any negative pct shuts enforcement off, so forged mail under a p=reject policy is delivered.
Both directions fail open. The exposed party is the domain owner who publishes the record, along with everyone who receives mail claiming to be from that domain. The attacker needs the ability to influence the victim's published DMARC record; there is no path here that works from the outside without it. An administrator typing an extra digit produces the same result without any attacker at all.
Parse pct into a type wide enough for the input, or detect overflow during the conversion. Range-check against 0 to 100 before any modular reduction happens, not after. On Rspamd, reject a negative value rather than clamping it. Neither implementation should substitute a sampling rate the record does not contain.
The half of this that reads "at or above 2^31 gives permerror, which fails open" overlaps opendmarc-bad-tag-drops-policy, which already lists an out-of-range pct among its permerror triggers. Two things are new here. The first is the 32-bit truncation model and the hole it leaves, where an out-of-range value produces an enforcing policy instead of a permerror. The second is the Rspamd negative clamp, which is a different implementation and a different defect.