Skip to content

Denial of service in mail-parser via attacker-controlled header names

High
fedelemantuano published GHSA-78xv-5vfh-jh5q Aug 12, 2026

Package

pip mail-parser (pip)

Affected versions

<= 4.6.1

Patched versions

4.6.2

Description

Impact

Parsing a single small email consumes seconds to minutes of CPU, or exhausts available
memory. Every input is attacker-controlled and no authentication is required, so any
service that parses untrusted mail — mail gateways, abuse pipelines, IMAP fetchers, upload
and API ingestion endpoints — can be denied service by one message.

Four distinct amplification primitives share one root cause: header names chosen by the
sender were resolved through Python attribute lookup, and the caller-facing conveniences of
that API (_json / _raw suffixes) were applied to them.

1. Quadratic CPU per distinct header name (CWE-407, reported by @iam-niranjan)

_make_mail() resolved one value per distinct header name, and each resolution rescanned
the whole header list via Message.get_all() — O(distinct × total). Cost tracks the number
of distinct names, not message size.

distinct names size 4.6.1 4.6.2
4,000 44 KB 0.363 s 0.011 s
8,000 88 KB 1.482 s 0.015 s
16,000 176 KB 6.832 s 0.031 s
32,000 repeats of one name 192 KB 0.057 s 0.045 s

Postfix's default header_size_limit of 102,400 bytes admits roughly 14,600 distinct
three-character names, giving about 50× amplification through a hardened MTA. Ingestion
paths with no header cap are unbounded.

import mailparser
headers = "".join(f"X{i:05d}: v\r\n" for i in range(16000))
mailparser.parse_from_string(f"From: a@b.c\r\n{headers}\r\nbody\r\n")

2. Unbounded recursion via a Headers_json header (CWE-674)

The headers property guarded against self-recursion by excluding the name headers from
its key set, but not the _json alias. A header named Headers_json made the property
re-enter itself, and each cycle rebuilt the entire header dictionary — multiplying against
primitive 1. Parsing then fails outright with MailParserRecursionError.

filler headers size 4.6.1 4.6.2
800 8 KB 0.917 s 0.002 s
1,600 16 KB 18.578 s 0.003 s
3,200 32 KB 48.312 s 0.006 s

About 1500× amplification — 30× more efficient than primitive 1 — well inside any MTA
header limit. It fires during parse() itself, so it cannot be avoided by not calling
mail_json. Headers_json_json behaves identically.

import mailparser
filler = "".join(f"Z{i:04d}: v\r\n" for i in range(1600))
mailparser.parse_from_string(f"Headers_json: x\r\n{filler}\r\nbody\r\n")

3. Exponential memory amplification via a _json-suffixed name (CWE-405)

Each _json suffix re-serialised the previous result, and json.dumps escapes every quote
and backslash, so value length follows L → 2L + 2: five input bytes buy one doubling of
the output. A 148-byte header produced a 536 MB string in 1.24 s. Amplification is additive
across headers and every value is retained:

headers × depth input retained amplification
10 × 18 972 B 5.2 MB 5,394×
20 × 20 2,152 B 41.9 MB 19,490×
40 × 22 4,712 B 335.5 MB 71,211×

Roughly 7 KB of headers reaches ~80 GB. The blowup happens inside a C-level json.dumps
that does not deliver signals, so a wall-clock timeout does not interrupt it.

import mailparser
name = "X" + "_json" * 28                    # 148 bytes
print(len(mailparser.parse_from_string(f"{name}: x\r\n\r\n").mail[name.lower()]))
# 4.6.1 -> 536870910      4.6.2 -> 1

4. Quadratic blowup in headers via case variants (CWE-407)

Header names compare case-insensitively, so every capitalisation resolves to the same full
value list — but headers keyed on the exact spelling sent. n capitalisations of one name
produced n keys, each holding an n-element list. A 20-character name offers 2²⁰
capitalisations at ~26 bytes each.

case variants size 4.6.1 4.6.2
2,000 52 KB 0.884 s 0.004 s
4,000 104 KB 2.917 s 0.008 s
8,000 271 KB 7.565 s 0.010 s
16,000 540 KB 30.24 s 0.004 s

Also an output amplifier: 271 KB of headers produced 540 MB of JSON, and
mail-parser -f bomb.eml -r produced 302 MB of output from a 198 KB file.

import mailparser
base = "x-aaaaaaaaaaaaaaaaaaa"
variants = ("".join(c.upper() if (i >> j) & 1 else c for j, c in enumerate(base))
            for i in range(4000))
mailparser.parse_from_string("".join(f"{v}: v\r\n" for v in variants) + "\r\nb\r\n").headers

Patches

Fixed in 4.6.2. Upgrade with pip install --upgrade mail-parser.

Header values now resolve through a literal-lookup-only resolver backed by a per-parse
index, so every lookup is O(1), no header name can re-enter a property, and the _json /
_raw suffixes are no longer interpreted on names taken from a message. headers
deduplicates names case-insensitively.

Workarounds

None. The issues are triggered by ordinary parsing of a well-formed message, so no
configuration change avoids them.

Operators who cannot upgrade immediately can reduce — but not eliminate — exposure by
capping header count and total header size before handing input to mail-parser, and by
running parsing in a subprocess with a memory rlimit. Note that a wall-clock timeout alone
does not stop primitive 3.

References

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
None
Integrity
None
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

CVE ID

No known CVE

Weaknesses

Asymmetric Resource Consumption (Amplification)

The product does not properly control situations in which an adversary can cause the product to consume or produce excessive resources without requiring the adversary to invest equivalent work or otherwise prove authorization, i.e., the adversary's influence is asymmetric. Learn more on MITRE.

Inefficient Algorithmic Complexity

An algorithm in a product has an inefficient worst-case computational complexity that may be detrimental to system performance and can be triggered by an attacker, typically using crafted manipulations that ensure that the worst case is being reached. Learn more on MITRE.

Uncontrolled Recursion

The product does not properly control the amount of recursion that takes place, consuming excessive resources, such as allocated memory or the program stack. Learn more on MITRE.

Credits