CVE-2026-21621 Overview
CVE-2026-21621 is an Incorrect Authorization vulnerability affecting the HexpmWeb.API.OAuthController module in hexpm/hexpm, the backend service powering Hex.pm, the package manager for the Elixir and Erlang ecosystems. This privilege escalation flaw allows attackers with read-only API key access to obtain full write permissions through improper scope handling during OAuth token exchange.
The vulnerability exists in the validate_scopes_against_key/2 routine within lib/hexpm_web/controllers/api/oauth_controller.ex. When a read-only API key (with domain: "api" and resource: "read") is exchanged via the OAuth client_credentials grant flow, the resource qualifier is incorrectly ignored. The resulting JWT receives the broad "api" scope instead of the expected "api:read" scope, effectively granting full API access.
Critical Impact
Attackers who obtain a victim's read-only API key and valid 2FA (TOTP) code can escalate to full write access, enabling them to publish, retire, or modify packages in the Hex.pm ecosystem.
Affected Products
- hexpm/hexpm from commit 71829cb6f6559bcceb1ef4e43a2fb8cdd3af654b
- hexpm/hexpm versions before commit 71c127afebb7ed7cc637eb231b98feb802d62999
- Hex.pm package registry infrastructure
Discovery Timeline
- 2026-03-05 - CVE-2026-21621 published to NVD
- 2026-03-05 - Last updated in NVD database
Technical Details for CVE-2026-21621
Vulnerability Analysis
This incorrect authorization vulnerability (CWE-863) stems from a flaw in how the OAuth controller validates and maps API key permissions to OAuth scopes. The validate_scopes_against_key/2 function failed to properly translate granular permission resources into the corresponding OAuth scope restrictions.
When building the set of allowed scopes from key permissions, the original implementation only checked the permission domain without considering the resource qualifier. For API domain permissions, it unconditionally returned the broad "api" scope regardless of whether the original key was restricted to read-only access. This architectural oversight meant that a key explicitly created with resource: "read" would be treated identically to a full-access key during OAuth token generation.
The attack requires two prerequisites: possession of a victim's read-only API key and a valid 2FA (TOTP) code for the victim's account. While this raises the bar for exploitation, read-only keys are often shared more liberally than full-access credentials, and TOTP codes can potentially be obtained through social engineering or other attack vectors.
Root Cause
The root cause lies in the validate_scopes_against_key/2 function which performed insufficient permission-to-scope mapping. The original code only mapped permission domains without respecting the resource qualifier:
# Original flawed logic
case permission.domain do
"api" -> ["api"] # Resource qualifier ignored!
"repository" -> ["repository:#{permission.resource}"]
"repositories" -> [:all_repositories]
_ -> []
end
This logic treated all API domain permissions identically, ignoring whether the permission was restricted to "read" or "write" operations. The fix introduces a new permission_to_scopes/1 function that properly respects the scope hierarchy.
Attack Vector
The attack is network-based and requires low privileges (possession of a read-only API key) and some prerequisites (valid 2FA code). An attacker follows this exploitation path:
- Obtain a victim's read-only API key (potentially from logs, shared CI/CD configurations, or credential exposure)
- Acquire a valid TOTP code for the victim's account
- Exchange the read-only API key via the OAuth client_credentials grant
- Receive a JWT with the broad "api" scope instead of "api:read"
- Use the elevated JWT to create a new full-access API key without expiration
- Perform write operations such as publishing malicious packages or retiring legitimate ones
The security patch introduces proper scope hierarchy handling:
+ def permission_to_scopes(%{domain: "api", resource: nil}),
+ do: ["api", "api:read", "api:write"]
+
+ def permission_to_scopes(%{domain: "api", resource: "write"}),
+ do: ["api:write", "api:read"]
+
+ def permission_to_scopes(%{domain: "api", resource: "read"}), do: ["api:read"]
+
+ def permission_to_scopes(%{domain: "repository", resource: resource}),
+ do: ["repository:#{resource}"]
+
+ def permission_to_scopes(%{domain: "repositories"}), do: [:all_repositories]
+ def permission_to_scopes(_permission), do: []
Source: GitHub Commit Update
Detection Methods for CVE-2026-21621
Indicators of Compromise
- JWT tokens with "api" scope originating from keys that were created with read-only permissions
- Unexpected API key creation events, especially keys with unrestricted permissions or no expiration
- Package publish, retire, or modification events from accounts that should only have read access
- OAuth token exchange requests followed immediately by privileged write operations
Detection Strategies
- Audit OAuth token exchange logs for scope escalation patterns where the input key has resource: "read" but the resulting JWT has full "api" scope
- Monitor for newly created API keys with full write permissions, particularly those without expiration dates
- Implement alerting on package registry write operations (publish, retire, modify) and correlate with the originating API key's expected permissions
- Review authentication logs for suspicious patterns combining TOTP validation with OAuth token exchanges
Monitoring Recommendations
- Enable detailed audit logging for all OAuth client_credentials grant operations
- Implement scope validation monitoring that alerts when JWT scopes exceed the source key's permissions
- Track API key lineage to detect when elevated keys are created using tokens from restricted keys
- Monitor for bulk or unusual package modifications that may indicate compromised write access
How to Mitigate CVE-2026-21621
Immediate Actions Required
- Update hexpm to commit 71c127afebb7ed7cc637eb231b98feb802d62999 or later immediately
- Audit all recently created API keys for unexpected full-access permissions
- Review package modification history for any unauthorized changes
- Consider rotating API keys for sensitive accounts, especially those with package publishing privileges
- Enable additional monitoring on OAuth token exchange endpoints
Patch Information
The vulnerability is fixed in commit 71c127afebb7ed7cc637eb231b98feb802d62999. The patch introduces the permission_to_scopes/1 function in lib/hexpm/permissions.ex that properly respects the permission hierarchy, ensuring read-only keys can only generate "api:read" scoped JWTs. The validate_scopes_against_key/2 function in the OAuth controller is updated to use this new function.
For detailed patch information, see the GitHub Security Advisory GHSA-739m-8727-j6w3.
Workarounds
- Restrict distribution of read-only API keys and treat them with similar sensitivity to full-access keys until patched
- Implement additional authentication factors or IP restrictions for OAuth token exchange endpoints
- Temporarily disable the OAuth client_credentials grant flow if not critical to operations
- Monitor and alert on any API key creation operations until the patch is deployed
# Example: Audit API keys created recently for unexpected permissions
# Review hexpm database for keys created in the last 30 days
mix run -e "
Hexpm.Accounts.Key
|> Hexpm.Repo.all()
|> Enum.filter(&(DateTime.compare(&1.inserted_at, DateTime.add(DateTime.utc_now(), -30, :day)) == :gt))
|> Enum.each(&IO.inspect(&1.permissions))
"
Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.

