Lemur: Unchecked `replaces[]` lets any user silence notifications and hijack auto-rotation for arbitrary certificates
Summary
Repo under test: https://github.com/Netflix/lemur
The certificate create and upload endpoints accept a replaces[] (alias replacements) array that is resolved to live Certificate ORM objects with no ownership or CertificatePermission check on the referenced certificates. The SQLAlchemy Certificate.replaces append listener then immediately sets victim.notify = False and populates victim.replaced. From that point the victim certificate is excluded from auto-reissue, its expiration notifications are silenced, and the periodic certificate_rotate Celery task deploys the attacker's certificate (endpoint.certificate.replaced[0]) onto every endpoint serving the victim certificate.
Any authenticated non-read-only user can therefore silently substitute their own certificate onto production load balancers and Kubernetes secrets they hold no role on, while suppressing the legitimate certificate's lifecycle automation.
Affected route
POST /api/1/certificates
POST /api/1/certificates/upload
PUT /api/1/certificates/<id>
Affected code
lemur/certificates/schemas.py:402—replaces = fields.Nested(AssociatedCertificateSchema, missing=[], many=True)accepted on create/upload/editlemur/schemas.py:152—AssociatedCertificateSchemaresolves any certificate by id/name viafetch_objects(Certificate, data)with no permission checklemur/certificates/views.py:651— onlyStrictRolePermission().can()gates/certificates/upload; no check ondata['replaces']lemur/certificates/models.py:506—@event.listens_for(Certificate.replaces, 'append')setsvalue.notify = Falseon the victimlemur/certificates/service.py:277—get_all_pending_reissue()filtersnot_(Certificate.replaced.any()), excluding the victimlemur/certificates/cli.py:347—request_rotation(endpoint, endpoint.certificate.replaced[0], message, commit)deploys the attacker certlemur/common/celery.py:638— periodiccertificate_rotatetask runscli_certificate.rotate(..., commit=True)lemur/deployment/service.py:17—endpoint.source.plugin.update_endpoint(endpoint, new_cert)pushes to ELB/CloudFront/ACM/K8s
Impact
An authenticated insider or holder of a stolen low-privilege token can, without holding any role on a target certificate:
- Upload a self-signed or attacker-minted certificate listing arbitrary high-value production certificate IDs in
replaces. - Immediately disable expiration notifications and auto-reissue for those production certificates.
- On the next scheduled
certificate_rotateCelery run, have the attacker's certificate pushed to every endpoint (AWS ELB/CloudFront/ACM, Kubernetes, SFTP, etc.) currently serving the victim certificate, while the legitimate certificate is detached.
Minimum impact is fleet-wide TLS denial of service equivalent to mass revocation. Where internal clients trust the substituted chain (or combined with the sub-CA finding LEMUR-BUG-07), it escalates to TLS interception. This directly violates the invariant that a user may only modify or revoke a certificate if they are its owner, a member of an owning role, or an administrator.
Root cause
AssociatedCertificateSchema.get_object calls fetch_objects(Certificate, data) and returns the ORM rows verbatim. No caller on the create/upload/edit path iterates the resolved replaces list to enforce CertificatePermission before the model assigns them, and the Certificate.replaces append event listener mutates the victim row (notify = False) as a side effect of ORM collection assignment. The direct revoke endpoint does enforce CertificatePermission, but this replaces path achieves an equivalent or worse outcome while bypassing it entirely.
Validated evidence
Static trace, confirmed by code inspection (validation status: CONFIRMED):
replacesis accepted inCertificateInputSchema/CertificateUploadInputSchemaand resolved viafetch_objects(Certificate, ...)with no per-object authorization.grep -n CertificatePermission lemur/certificates/views.pyshows the check is applied toPUT/DELETE/revoke/exportpaths but never to thereplacespayload ofPOST /certificatesorPOST /certificates/upload.- The Celery
certificate_rotatetask andcli.rotate()consumeEndpoint.replaced.any()unconditionally and deployreplaced[0]withcommit=True.
Proof of concept / reproducer
Status: reconstructed from source report (static control-flow trace; not executed against a live CA).
Preconditions: attacker is an authenticated Lemur user holding any role other than read-only (default StrictRolePermission config). <VICTIM_CERT_ID> is any certificate id readable via GET /api/1/certificates.
# 1. Upload an attacker-controlled cert that "replaces" the victim
curl -sS -X POST "<TARGET_BASE_URL>/api/1/certificates/upload" \
-H "Authorization: Bearer <AUTH_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"name": "attacker-replacement",
"owner": "attacker@example.com",
"body": "-----BEGIN CERTIFICATE-----\n<ATTACKER_CERT_PEM>\n-----END CERTIFICATE-----",
"privateKey": "-----BEGIN PRIVATE KEY-----\n<ATTACKER_KEY_PEM>\n-----END PRIVATE KEY-----",
"replaces": [{"id": <VICTIM_CERT_ID>}]
}'
# 2. Observe victim.notify is now false and victim is queued for rotation
curl -sS "<TARGET_BASE_URL>/api/1/certificates/<VICTIM_CERT_ID>" \
-H "Authorization: Bearer <AUTH_TOKEN>" | jq '.notify, .replaced'
# 3. On the next certificate_rotate Celery beat tick, the attacker cert is
# deployed to every endpoint that was serving <VICTIM_CERT_ID>.
Static-trace validation command from the source report:
grep -n 'replaces' lemur/certificates/schemas.py lemur/certificates/views.py lemur/schemas.py \
&& grep -n 'CertificatePermission' lemur/certificates/views.py
Source artifact: audit/harnesses/public-repo-threat-model-harness/results/netflix-lemur-100run-mythos-20260627T051129Z/findings.jsonl (run_083, finding cluster lemur-replaces-unauth, 7/100 runs).
Suggested fix
Before persisting replaces/replacements on certificate create, upload, and edit, iterate each referenced certificate and enforce the same CertificatePermission(owner_role, cert.roles) check used by the revoke endpoint (views.py:1677-1685); reject with 403 if the caller is not creator/owner/role-member/admin for any target. Additionally, move the value.notify = False side effect out of the SQLAlchemy append listener so an authorization failure cannot leave a victim certificate partially mutated, and emit an audit_log entry whenever a certificate is marked as replaced.
Software afectado
Haz clic en una tecnología para ver todas sus vulnerabilidades.
| Plataforma | Paquete | Versiones vulnerables | Corregida en |
|---|---|---|---|
| Python (PyPI) | lemur | >= 0.5.0, < 1.9.3 | 1.9.3 |
Referencias
- https://github.com/Netflix/lemur/security/advisories/GHSA-cfh6-pv5c-38jv
- https://github.com/Netflix/lemur/commit/286874535160952143b0afe2d356642669f9d4c6
- https://github.com/Netflix/lemur
- https://github.com/Netflix/lemur/releases/tag/v1.9.3
- https://pypi.org/project/lemur
- https://github.com/advisories/GHSA-cfh6-pv5c-38jv
- https://nvd.nist.gov/vuln/detail/CVE-2026-71308
- Vector CVSS
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H- Debilidad (CWE)
CWE-639, CWE-862- Plataforma
- Python (PyPI)
- Paquete
lemur- Publicada
- 18.08.2026
- Actualizado
- 11.10.2026 14:19
Fuentes y licencias
OSV / GitHub Advisory Database - PYSEC-2026-3675 - PyPA Advisory Database; CC BY 4.0
OSV / GitHub Advisory Database - GHSA-cfh6-pv5c-38jv - CC BY 4.0
Historial de vulnerabilidades: lemur
Todas las vulnerabilidades publicadas de este paquete (Python (PyPI)), de la más reciente a la más antigua. Muestra si es un fallo aislado o un paquete que hay que vigilar.
- Entradas en total
- 18
- Primera → última
- 08.2017 08.2026
- Sin corrección
- 0
- Explotada en ataques
- 0
Distribución por gravedad
- Crítica 1
- Alta 9
- Media 8
Divulgaciones por año
- 2017: 1
- 2018: 0
- 2019: 0
- 2020: 0
- 2021: 0
- 2022: 0
- 2023: 1
- 2024: 0
- 2025: 0
- 2026: 16
-
Lemur: Unchecked `replaces[]` lets any user silence notifications and hijack auto-rotation for arbitrary certificates esta entrada
Identifica las vulnerabilidades de tu aplicación
Con la integración de DockRay y el inventario de software activado, DockRay compara las versiones notificadas de los componentes con las vulnerabilidades conocidas y avisa de las coincidencias confirmadas. La cobertura depende de la tecnología compatible y de los avisos disponibles.