Safeguard
Vulnerability Analysis

Python email module address-parsing confusion (CVE-2023-27043)

CVE-2023-27043 shows how a Python email-parsing quirk lets attackers spoof trusted domains and bypass allowlist-based access controls silently.

Vikram Iyer
Cloud Security Engineer
9 min read

A parsing bug deep inside Python's standard library email module — tracked as CVE-2023-27043 — lets attackers craft address headers that Python's own email.utils.parseaddr() and email.utils.getaddresses() read differently than a human, a mail transfer agent, or the very application relying on them ever intended. The practical effect: applications that gate access on the domain portion of an email address (think "only @company.example.com addresses may register") can be tricked into approving an address that actually belongs to an attacker-controlled domain, because the parser silently returns the wrong half of the header as the "email address" field. It's not a memory-corruption bug or a remote code execution primitive — it's a semantic confusion bug, and those are often more dangerous precisely because nothing crashes, nothing throws, and the code keeps running with quietly wrong data.

Affected versions and components

The vulnerable code lives in Lib/email/_parseaddr.py, the internal address-tokenizing engine behind the public email.utils helpers that a huge fraction of the Python ecosystem uses to read From, To, Cc, and Reply-To headers.

  • email.utils.parseaddr() — splits a single RFC 2822/5322 address header into a (realname, email_address) tuple.
  • email.utils.getaddresses() — does the same for a list of headers, returning a list of tuples.

According to the CPython issue tracker (GH-102988) and the CVE record, the flaw affects Python's email module "through 3.11.3," and the underlying parsing logic was effectively unchanged — and vulnerable in the same way — across the 2.7 line and every Python 3 branch up through 3.12 at the time of disclosure. This is not a narrow edge case limited to one minor version: it's a structural weakness in how the tokenizer decides where the "real name" portion of a header ends and the "addr-spec" (the actual address) begins when the input contains RFC-special characters — ()<>@,:;.\"[] — in unexpected positions.

Any application that uses parseaddr() or getaddresses() output to make a trust decision is potentially exposed. That includes, in practice:

  • Signup/onboarding flows that restrict registration to specific corporate email domains.
  • Ticketing and helpdesk systems that authorize actions based on the sender domain of an inbound email.
  • Mailing-list and mail-relay software that whitelists or blacklists senders by parsed address.
  • Any SIEM, SOAR, or email-security tooling written in Python that extracts the "real" sender address for correlation, allow-listing, or reputation scoring.
  • Internal tooling built on smtplib, imaplib, Django's or Flask's email utilities, or any framework that delegates address parsing to the standard library rather than a strict RFC 5322 grammar validator.

Because email.utils sits underneath so much of the Python mail-handling ecosystem, this is the kind of finding that's easy to miss in a dependency review — nobody thinks of the standard library as a "third-party dependency" that needs a CVE watch, but it absolutely is one.

CVSS, EPSS, and KEV context

NVD and downstream vendor advisories (Red Hat, SentinelOne, Ubuntu) place CVE-2023-27043 at a CVSS v3.1 base score of 5.3 (Medium), reflecting a network-exploitable, low-complexity issue with no privileges or user interaction required, but with impact limited to confidentiality of the affected security control rather than direct data destruction or code execution (mapped to CWE-20: Improper Input Validation). The EPSS score sits at roughly 0.18%, indicating a low predicted probability of near-term mass exploitation — consistent with a logic-confusion bug that requires an application to have built a specific, brittle trust assumption on top of the parser rather than a broadly wormable flaw. As of this writing, CVE-2023-27043 has not been added to the CISA Known Exploited Vulnerabilities (KEV) catalog, and no confirmed in-the-wild exploitation has been publicly reported.

That said, low EPSS and absent-from-KEV should not be read as "ignore this." EPSS models opportunistic, internet-scale exploitation of a vulnerability in isolation; it does not model an attacker who has specifically identified that your signup flow, ticketing system, or SOC tooling trusts parseaddr() output for an authorization decision. Business-logic bypasses like this one are frequently discovered by targeted red-team or bug-bounty research precisely because they don't show up in generic vulnerability scanners — they require someone to trace how parsed address data actually gets used downstream. If your organization has any application logic that says, in effect, "if the parsed domain equals X, grant access," this bug deserves attention regardless of its EPSS percentile.

Timeline

  • March 24, 2023 — The issue is reported publicly on the CPython issue tracker as GH-102988, "Parsing errors in email/_parseaddr.py lead to incorrect value in email address part of tuple," describing how special characters in an address header cause the real-name portion to be returned in the email-address field of the tuple.
  • Early-to-mid 2023 — CVE-2023-27043 is assigned and published, with NVD, Red Hat, and Ubuntu tracking entries created for the affected Python 2.7 and 3.x branches. Linux distributions begin tracking the flaw as a low-urgency, "won't fix immediately without upstream consensus" item, since a naive fix risked breaking backward compatibility for any code that depended on the old (lenient) parsing behavior.
  • Through 2023 into early 2024 — Upstream discussion converges on introducing a new strict keyword argument to parseaddr() and getaddresses() rather than silently changing default behavior outright — a deliberate compatibility-preserving design so existing callers aren't broken without warning.
  • 2024 — The fix lands across the actively maintained branches (Python 3.13 mainline first, then backported via separate pull requests to 3.12, 3.11, 3.10, 3.9, and 3.8). With strict=True (the new default once shipped), malformed inputs that previously produced a misleading (realname, email) tuple now correctly return an empty ('', '') tuple instead, signaling a parse failure rather than fabricating a plausible-looking but wrong result. A supports_strict_parsing attribute was added so libraries can detect whether the running interpreter has the fix.
  • 2024 — Downstream distributions (Red Hat Enterprise Linux 8 and 9, Fedora, Ubuntu, Debian) ship backported patches across their supported Python 3.6, 3.9, and 3.11 packages, with associated RHSA/RHBA advisories issued through the first half of the year.

Worth noting for context: this is not the first time this exact class of bug has surfaced in the same code path — an earlier, related issue (CVE-2019-16056) addressed similar address-parsing confusion years earlier, and the recurrence underscores how difficult it is to fully harden a lenient, backward-compatible RFC 2822 tokenizer without a rewrite. Treat any application logic built on email.utils parsing as an area worth periodic re-review, not a one-time patch-and-forget.

Remediation steps

  1. Upgrade Python. Move to a patched interpreter release that includes the strict parsing fix: Python 3.13 and later ship it by default; backports are available in 3.12.4+, 3.11.9+, 3.10.14+, 3.9.19+, and 3.8.19+ (verify exact patch versions against your distribution's advisories, since Linux vendors backport on their own cadence). For EOL branches (2.7, and any 3.x line your org still runs past its support window), you cannot get an upstream fix — prioritize migration.
  2. Explicitly pass strict=True. Once running a patched interpreter, don't assume the default has flipped in your environment — distro packages and virtualenvs can differ. Explicitly call email.utils.parseaddr(header, strict=True) and email.utils.getaddresses(headers, strict=True) wherever the result feeds a security decision, and check getattr(email.utils, "supports_strict_parsing", False) at startup so your application fails loudly on an unpatched interpreter instead of silently falling back to lenient parsing.
  3. Handle the new empty-tuple failure mode. With strict parsing, malformed input now returns ('', '') rather than raising — audit every call site to make sure your code treats an empty result as "reject, log, and alert," not as "no real name, but the email field is still fine to trust."
  4. Never make trust decisions on parser output alone. Treat parseaddr()/getaddresses() results as untrusted input to a second validation layer: apply a strict RFC 5322/5321 grammar check (or a dedicated validator library) and, where domain trust actually matters, verify via SPF/DKIM/DMARC alignment or an authenticated identity provider rather than string-matching a parsed domain.
  5. Audit downstream dependencies, not just your own code. Any library or framework in your dependency tree that wraps email.utils internally — mail parsers, ticketing SDKs, notification libraries — inherits this exposure. A version bump of your interpreter doesn't help if a vendored or pinned dependency ships its own bundled email-parsing logic.
  6. Add a regression test with the special-character payloads. Bake a test into CI that feeds addresses containing ()<>@,:;.\"[] combinations through your actual authorization path and asserts the expected reject/accept outcome, so a future dependency bump or interpreter downgrade can't silently reintroduce lenient behavior.

How Safeguard Helps

Standard-library CVEs like this one are exactly where generic SCA scanning falls short: a scanner will flag "Python interpreter is outdated," but it can't tell you whether your codebase actually calls parseaddr() on a security-relevant path, or whether that call is reachable from an unauthenticated signup endpoint. Safeguard's reachability analysis traces the real call graph from your entry points down into email.utils usage, so you know immediately whether CVE-2023-27043 is exploitable in your specific application or just present-but-inert in an unused corner of a dependency. Griffin, Safeguard's AI-driven analysis engine, reviews the surrounding logic to surface exactly which authorization or domain-allowlist checks consume the parsed address, so triage doesn't require a manual code archaeology exercise across every service. Safeguard's SBOM generation and ingest give you a live inventory of which interpreter versions and email-handling dependencies are running across your fleet, closing the gap where a patched base image still ships a vendored, unpatched parser. And where the fix is mechanical — pinning a patched Python version, adding strict=True, or updating a lockfile — Safeguard can open an auto-fix pull request directly against the affected repository, turning a standard-library advisory into a reviewed, mergeable change instead of a ticket that sits in a backlog.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.