GitHub Advisoryadvisory · PyPI

GHSA-pxmc-2ffp-8j67 Lemur: Any user can revoke arbitrary certificates at the CA by uploading a duplicate record and revoking it

HIGH Published 2026-08-18 Modified 2026-08-18 Official page ↗
Aliases
CVE-2026-71417
Ecosystem
PyPI
Packages
lemur
Severity
High
CVSS vector
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:N/I:L/A:H
Weaknesses
CWE-639
Reviewed
2026-08-18

Affected packages

• PyPI lemur: < 1.9.3 → fixed in 1.9.3

Description

Summary Repo under test: https://github.com/Netflix/lemur `PUT /api/1/certificates//revoke` authorizes the caller against the *Lemur database row* (creator == current user, or `CertificatePermission` over the row's roles) rather than the underlying CA-side certificate identity. Separately, `POST /api/1/certificates/upload` lets any user passing `StrictRolePermission` create a new `Certificate` row while freely supplying `body`, `authority` (resolved by id/name with no `AuthorityPermission` check) and `external_id`; there is no uniqueness constraint on `body`, `serial`, or `external_id`. An attacker can therefore read a target certificate's public `body`, `authority.id`, and `external_id` via `GET /certificates/`, upload a duplicate row, and revoke that duplicate. The creator-bypass skips `CertificatePermission`, the empty-endpoints check passes because the duplicate has none, and `service.revoke()` then revokes at the CA using the attacker-supplied `body` (ACME) or `external_id` (DigiCert/Entrust/Google CA/CFSSL) under the authority's stored CA credentials — revoking the real production certificate. Affected route `POST /api/1/certificates/upload` → `PUT /api/1/certificates//revoke` Affected code • `lemur/certificates/views.py:651` — only `StrictRolePermission().can()` gates upload; no `AuthorityPermission` check • `lemur/certificates/schemas.py:391` — `CertificateUploadInputSchema` accepts caller-supplied `authority` and `external_id` • `lemur/schemas.py:107` — `AssociatedAuthoritySchema` resolves any authority by id/name with no permission check • `lemur/certificates/service.py:489` — `upload()` binds the caller-supplied authority onto the new row • `lemur/certificates/models.py:119` — only `name` is unique; `body`/`serial`/`external_id` are not, and `get_or_increase_name()` auto-suffixes on collision • `lemur/certificates/views.py:1677` — `if g.current_user != cert.user: ... CertificatePermission ...` — creator of the duplicate row bypasses the owner check • `lemur/certificates/views.py:1687` — `if cert.endpoints: ...` — duplicate row has no endpoints, so the deployed-cert safeguard is bypassed • `lemur/certificates/service.py:1120` — `plugin = plugins.get(certificate.authority.plugin_name); plugin.revoke_certificate(certificate, reason)` • `lemur/plugins/lemur_acme/acme_handlers.py:268` — ACME revokes by `certificate.body` • `lemur/plugins/lemur_digicert/plugin.py:477`, `lemur/plugins/lemur_entrust/plugin.py:321` — commercial CAs revoke by `certificate.external_id` • `lemur/certificates/schemas.py:290` — `CertificateOutputSchema` exposes `external_id`, `body`, `authority` to any authenticated user Impact A low-privileged authenticated insider (or holder of a stolen non-admin token/API key) can revoke any certificate managed by Lemur — including high-value certificates they do not own and certificates currently attached to live endpoints — directly at the issuing CA. Iterating over `GET /certificates` yields fleet-wide revocation (mass DoS of TLS endpoints) without ever passing an `AuthorityPermission` or `CertificatePermission` check on the victim certificate. This is exactly the "Revocation as DoS vector" scenario flagged in the threat model and additionally defeats the built-in "cannot revoke while attached to endpoint" safeguard. Root cause Revocation authority is bound to ownership of the Lemur DB row, not to the CA-side certificate identity. Because upload allows creating a second row that aliases the same CA-side certificate (same `body` / `external_id` / `authority`) without any uniqueness constraint or `AuthorityPermission` check, the attacker can manufacture a row they own and then exercise the creator-bypass on revoke. The endpoint-attached guard inspects only the duplicate row's `cert.endpoints`, which is empty. Validated evidence Static path trace, confirmed by code inspection (validation status: `CONFIRMED`): • Upload is gated only by `StrictRolePermission` (default-open to any non-read-only user) and accepts attacker-chosen `authority` + `external_id` + `body` without an `AuthorityPermission` check. • `Certificate.body` / `external_id` have no uniqueness constraint, so a duplicate row is created. • The revoke endpoint short-circuits the owner check when caller is the row creator, and the endpoint-attached guard inspects only the duplicate row. • Issuer plugins revoke at the CA using `body` / `external_id` taken from the duplicate row under the authority's stored CA credentials. Proof of concept / reproducer Status: reconstructed from source report (static control-flow trace; not executed against a live CA — revoking at a real CA is destructive). Preconditions: attacker is an authenticated Lemur user holding any role other than `read-only`. `` is any certificate id readable via `GET /api/1/certificates`. ```bash 1. Read the victim's body / authority / external_id (exposed to any authenticated user) curl -sS "/api/1/certificates/" \ -H "Authorization: Bearer " \ | jq '{body, external_id, authority: .authority.id}' 2. Upload a duplicate row aliasing the same CA-side certificate curl -sS -X POST "/api/1/certificates/upload" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "name": "victim-dup", "owner": "[email protected]", "body": "", "authority": {"id": }, "externalId": "" }' → returns {"id": , ...}; attacker is now cert.user of 3. Revoke the duplicate — issuer plugin revokes at the CA by body/external_id curl -sS -X PUT "/api/1/certificates//revoke" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{"crlReason": "unspecified"}' ``` Static-trace validation command from the source report: ```bash grep -n 'StrictRolePermission' lemur/certificates/views.py | grep -v Authority grep -n 'cert.authority = kwargs.get' lemur/certificates/service.py grep -n 'g.current_user != cert.user' lemur/certificates/views.py grep -n 'certificate.external_id\|certificate.body' \ lemur/plugins/lemur_acme/acme_handlers.py \ lemur/plugins/lemur_digicert/plugin.py \ lemur/plugins/lemur_entrust/plugin.py ``` Source artifact: `audit/harnesses/public-repo-threat-model-harness/results/netflix-lemur-100run-mythos-20260627T051129Z/findings.jsonl` (run_050, finding cluster `lemur-revoke-via-duplicate`, 2/100 runs). Suggested fix Decouple CA-side revocation authority from Lemur row ownership: 1. On `POST /certificates/upload`, if `authority` is supplied enforce `AuthorityPermission` for that authority, and reject/ignore caller-supplied `external_id`. 2. Before calling `plugin.revoke_certificate`, look up all `Certificate` rows sharing the same `(authority_id, serial)` or `body` and require `CertificatePermission` on every match (and run the en…

Related

Alias · CVE (1)

  • CVE-2026-71417 Lemur manages TLS certificate creation. vulnerability

Weakness · CWE (1)

  • CWE-639 Authorization Bypass Through User-Controlled Key weakness

References