Vulnerability analysis · Content filtering

Apache SpamAssassin scan time grows with the square of HTML nesting depth, stalling mail processing [Apache SpamAssassin 3.4.6, latest release checked 2026-07-30]

Yongzhe Xu, Virginia Tech  ·  yongzhe@vt.edu  ·  2026-07-31

Doubling the nesting depth of a message body quadruples the scan time. A 220 KB body of nested <div> elements holds the scanner for over a minute.

SoftwareApache SpamAssassin
VendorThe Apache Software Foundation (Apache SpamAssassin)
Sourcehttps://github.com/apache/spamassassin
Affected3.4.6 (the version tested). Other releases sharing the same HTML processing are likely affected; not yet verified against 4.x. Newest published release checked on 2026-07-30.
WhereHTML body processing, Mail::SpamAssassin::HTML and its HTML::Parser usage; the exact quadratic loop is not yet pinpointed.
WeaknessCWE-407 (Inefficient Algorithmic Complexity)
Reachable byRemote, unauthenticated. One inbound e-mail; SpamAssassin scans the HTML body of every message.
CVSS v3.1CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H (7.5 (High))
VerificationConfirmed by timing measurement across four nesting depths, with a flat-body control that isolates depth from size.

1. Overview

SpamAssassin spends time on an HTML body in proportion to the square of its nesting depth. A body built from deeply nested <div> elements holds the scanner for minutes. A flat body many times larger finishes in seconds. Every inbound message gets scanned and the body is entirely under the sender's control, so one message of moderate size is enough to stall mail processing.

2. Analysis

Scan times were measured at four nesting depths. The message size is shown alongside each.

Nested <div> depthScan timeMessage size
2,0003.2 s~22 KB
5,0007.9 s~55 KB
10,00031.8 s~110 KB
20,000> 60 s (timeout)~220 KB

Depth 5,000 takes 7.9 s. Doubling it to 10,000 takes 31.8 s, four times as long. That ratio is what an O(n²) algorithm produces.

A control separates depth from byte count. A flat 2 MB body, about ten times the size of the 220 KB nested case, scans in roughly 4 seconds. Size alone is therefore not the driver; the tree depth is. Ordinary large-input slowness does not explain the numbers.

The exact quadratic loop has not been pinpointed. It sits somewhere in Mail::SpamAssassin::HTML and its use of HTML::Parser.

3. Reproduction

printf 'From: a@b.com\nContent-Type: text/html\n\n' > poc
python3 -c "import sys; sys.stdout.buffer.write(b'<div>'*20000 + b'x' + b'</div>'*20000)" >> poc

time spamassassin --local -t < poc     # > 60 s on a ~220 KB message

The first command writes a minimal HTML message header. The second appends a body of 20,000 nested <div> elements. The third scans it and reports the elapsed time.

For a run that finishes rather than hitting the timeout, drop the repetition count to 10,000. That takes about 32 seconds.

4. Assessment

The effect is on availability, not memory safety; nothing crashes. With the default per-message timeout in place, a stream of these messages backs up the queue and delays delivery. Without an effective timeout, each message holds a processor core for minutes.

An attacker needs no authentication, no account, and no prior relationship with the target. The cost on their side is a single 220 KB message, which makes the amplification large.

5. Remediation

Bound the work performed, not just the size of the input.

A targeted fix requires locating the quadratic loop first. It may live in the tree construction or in a rule that walks the whole tree once per node. That work has not been done, so the measures above are limits rather than a repair.

6. Additional notes

Check for a duplicate before submission. SpamAssassin carries prior denial-of-service CVEs, among them CVE-2017-15705 and CVE-2018-11780. Whether an existing assignment already covers this nested-HTML quadratic has not been established. Raise it with the Apache SpamAssassin security team before requesting a new identifier.

The measurements come from 3.4.6 only. The current 4.x line has not been tested.

7. References