|
| 1 | +--- |
| 2 | +title: Preventing Domain Resurrection Attacks |
| 3 | +description: PyPI now checks for expired domains to prevent domain resurrection attacks, a type of supply-chain attack where someone buys an expired domain and uses it to take over PyPI accounts through password resets. |
| 4 | +authors: |
| 5 | + - miketheman |
| 6 | +date: 2025-08-18 |
| 7 | +tags: |
| 8 | + - security |
| 9 | +--- |
| 10 | + |
| 11 | +## Summary |
| 12 | + |
| 13 | +PyPI now checks for expired domains to prevent domain resurrection attacks, |
| 14 | +a type of supply-chain attack where someone buys an expired domain |
| 15 | +and uses it to take over PyPI accounts through password resets. |
| 16 | + |
| 17 | +These changes improve PyPI's overall account security posture, |
| 18 | +making it harder for attackers to exploit expired domain names |
| 19 | +to gain unauthorized access to accounts. |
| 20 | + |
| 21 | +<!-- more --> |
| 22 | + |
| 23 | +Since early June 2025, PyPI has unverified over 1,800 email addresses |
| 24 | +when their associated domains entered expiration phases. |
| 25 | +This isn't a perfect solution, but it closes off a significant attack vector |
| 26 | +where the majority of interactions would appear completely legitimate. |
| 27 | + |
| 28 | +## Background |
| 29 | + |
| 30 | +PyPI user accounts are linked to email addresses. |
| 31 | +Email addresses are tied to domain names; |
| 32 | +domain names can expire if unpaid, and someone else can purchase them. |
| 33 | + |
| 34 | +During PyPI account registration, [users are required to verify their email addresses](https://policies.python.org/pypi.org/Terms-of-Service/#2-required-information) |
| 35 | +by clicking a link sent to the email address provided during registration. |
| 36 | +This verification ensures the address is valid and accessible to the user, |
| 37 | +and may be used to send important account-related information, |
| 38 | +such as password reset requests, or for PyPI Admins to use to contact the user. |
| 39 | + |
| 40 | +PyPI considers the account holder's initially verified email address a strong indicator of account ownership. |
| 41 | +Coupled with a form of Two-Factor Authentication (2FA), this helps to further secure the account. |
| 42 | + |
| 43 | +Once expired, an attacker could register the expired domain, set up an email server, |
| 44 | +issue a password reset request, and gain access to accounts associated with that domain name. |
| 45 | + |
| 46 | +Accounts with any activity after [January 1 2024 will have 2FA enabled](2024-01-01-2fa-enforced.md), |
| 47 | +and an attacker would need to have either the second factor, |
| 48 | +or perform a full account recovery. |
| 49 | + |
| 50 | +For older accounts prior to the 2FA requirement date, |
| 51 | +having an email address domain expire could lead to account takeover, |
| 52 | +which is what we're attempting to prevent, |
| 53 | +as well as minimize potential exposure if an email domain _does_ expire and change hands, |
| 54 | +regardless of whether the account has 2FA enabled. |
| 55 | + |
| 56 | +This is not an imaginary attack - this has happened at least once for [a PyPI project](https://osv.dev/vulnerability/PYSEC-2022-199) back in 2022, |
| 57 | +and [other package ecosystems](https://blog.illustria.io/illustria-discovers-account-takeover-vulnerability-in-a-popular-package-affecting-1000-8aaaf61ebfc4). |
| 58 | + |
| 59 | +**TL;DR: If a domain expires, don't consider email addresses associated with it verified any more.** |
| 60 | + |
| 61 | +## Domain Expiration Timeframe |
| 62 | + |
| 63 | +Here's a generalized flowchart of phases that domain name registrars often adhere to. |
| 64 | +There's typically a grace period before a domain is deleted. |
| 65 | +Read more about [ICANN's Expired Registration Recovery Policy (ERRP)](https://www.icann.org/resources/pages/registrant-about-errp-2018-12-07-en). |
| 66 | + |
| 67 | +One way to visualize this can be seen in the below flowchart: |
| 68 | + |
| 69 | +```mermaid |
| 70 | +flowchart TD |
| 71 | + A[Domain Active] -->|Expiration Date Reached| B{Owner Renews?} |
| 72 | +
|
| 73 | + B -->|Yes - Within Grace Period| A |
| 74 | + B -->|No| C[Renewal Grace Period<br/>0-45 days] |
| 75 | +
|
| 76 | + C -->|Owner Renews<br/>Regular Price| A |
| 77 | + C -->|No Renewal| D[Redemption Period<br/>30 days] |
| 78 | +
|
| 79 | + D -->|Owner Redeems<br/>High Fee $70-200+| A |
| 80 | + D -->|No Redemption| E[Pending Delete<br/>5 days] |
| 81 | +
|
| 82 | + E -->|Automatic| F[Domain Released] |
| 83 | +``` |
| 84 | + |
| 85 | +The word "expiration" might be a bit overloaded, |
| 86 | +as conceptually there is no specific state advertised that a domain name has expired, |
| 87 | +rather using other indicators we can infer what state the domain name is currently in. |
| 88 | + |
| 89 | +Thanks to our friends at [Domainr](https://domainr.com/) (a Fastly service), |
| 90 | +we can use their [Status API](https://domainr.com/docs/api/v2/status#status-results) |
| 91 | +to issue periodic queries for any given domain, and act on the response. |
| 92 | + |
| 93 | +The time interval we've chosen for now is 30 days, as per the flow above, |
| 94 | +there's a high likelihood that a domain is still in the Renewal Grace Period |
| 95 | +or Redemption Period when we check for status, |
| 96 | +and can react before it is released or changes hands. |
| 97 | + |
| 98 | +_Note_: PyPI will not detect a non-expiring domain transfer, |
| 99 | +as we assume the parties are acting together to transfer a domain legitimately. |
| 100 | + |
| 101 | +## PyPI Actions |
| 102 | + |
| 103 | +After an initial bulk check period that took place in April 2025, |
| 104 | +PyPI will check daily for any domains in use for status changes, |
| 105 | +and update its internal database with the most recent status. |
| 106 | + |
| 107 | +If a domain registration enters the redemption period, |
| 108 | +that's an indicator to PyPI that the previously verified email destinations may not be trusted, |
| 109 | +and will un-verify a previously-verified email address. |
| 110 | +PyPI will not issue a password reset request to addresses that have become unverified. |
| 111 | + |
| 112 | + |
| 113 | + |
| 114 | +Since the initial implementation early June 2025, |
| 115 | +PyPI has unverified over 1,800 email addresses (initial 1,500 excluded from chart), |
| 116 | +and will continue to do so daily, to protect both PyPI account holders, |
| 117 | +as well as the end users of PyPI packages. |
| 118 | + |
| 119 | +## Recommendations for end users |
| 120 | + |
| 121 | +If your PyPI account only has a **single verified** email address from a custom domain name, |
| 122 | +add a **second verified** email address from another notable domain (e.g. Gmail) to your account. |
| 123 | + |
| 124 | +During a PyPI account recovery, PyPI may ask for other proofs, |
| 125 | +often via other services under the user's control. |
| 126 | +If the same email address is used on those other services, the recovery could appear legitimate. |
| 127 | +Ensure you have 2FA set on those services as well to prevent potential account takeovers. |
| 128 | + |
| 129 | +## That's all for now, folks |
| 130 | + |
| 131 | +While these changes are not foolproof, |
| 132 | +they decrease the likelihood of domain resurrections account takeovers. |
| 133 | + |
| 134 | +Thanks to Eric Case at [Fastly](https://www.fastly.com/) for helping us understand some of the complexities |
| 135 | +and Samuel Giddins at [Ruby Central](https://rubycentral.org/) for their initial ideas of this approach, |
| 136 | +and the [OpenSSF Securing Software Repositories Working Group](https://repos.openssf.org/) |
| 137 | +for their collaborative guidance on repository security. |
| 138 | + |
| 139 | +This effort would not be possible without the continued support from [Alpha-Omega](https://alpha-omega.dev/). |
| 140 | + |
| 141 | +### Related reading |
| 142 | + |
| 143 | +- [Wikipedia: Domain hijacking](https://en.wikipedia.org/wiki/Domain_hijacking) |
| 144 | +- [CAPEC-50: Password Recovery Exploitation](https://capec.mitre.org/data/definitions/50.html) |
| 145 | +- [CAPEC-695: Repo Jacking](https://capec.mitre.org/data/definitions/695.html) |
| 146 | +- [OpenSSF: Principles for Package Repository Security](https://repos.openssf.org/principles-for-package-repository-security) |
| 147 | +- [arxiv: What are Weak Links in the npm Supply Chain?](https://arxiv.org/abs/2112.10165) |
0 commit comments