GHSA-cfh6-pv5c-38jv: High severity pip/lemur vulnerability

Published Aug 18, 2026
·
Updated

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 certificaterotate 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/edit - lemur/schemas.py:152 — AssociatedCertificateSchema resolves any certificate by id/name via fetchobjects(Certificate, data) with no permission check - lemur/certificates/views.py:651 — only StrictRolePermission().can() gates /certificates/upload; no check on data['replaces'] - lemur/certificates/models.py:506 — @event.listensfor(Certificate.replaces, 'append') sets value.notify = False on the victim - lemur/certificates/service.py:277 — getallpendingreissue() filters not(Certificate.replaced.any()), excluding the victim - lemur/certificates/cli.py:347 — requestrotation(endpoint, endpoint.certificate.replaced[0], message, commit) deploys the attacker cert - lemur/common/celery.py:638 — periodic certificaterotate task runs clicertificate.rotate(..., commit=True) - lemur/deployment/service.py:17 — endpoint.source.plugin.updateendpoint(endpoint, newcert) 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:

1. Upload a self-signed or attacker-minted certificate listing arbitrary high-value production certificate IDs in replaces. 2. Immediately disable expiration notifications and auto-reissue for those production certificates. 3. On the next scheduled certificaterotate Celery 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.getobject calls fetchobjects(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):

- replaces is accepted in CertificateInputSchema / CertificateUploadInputSchema and resolved via fetchobjects(Certificate, ...) with no per-object authorization. - grep -n CertificatePermission lemur/certificates/views.py shows the check is applied to PUT/DELETE/revoke/export paths but never to the replaces payload of POST /certificates or POST /certificates/upload. - The Celery certificaterotate task and cli.rotate() consume Endpoint.replaced.any() unconditionally and deploy replaced[0] with commit=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). <VICTIMCERTID> is any certificate id readable via GET /api/1/certificates.

bash 1. Upload an attacker-controlled cert that "replaces" the victim curl -sS -X POST "<TARGETBASEURL>/api/1/certificates/upload" \ -H "Authorization: Bearer <AUTHTOKEN>" \ -H "Content-Type: application/json" \ -d '{ "name": "attacker-replacement", "owner": "attacker@example.com", "body": "-----BEGIN CERTIFICATE-----\n<ATTACKERCERTPEM>\n-----END CERTIFICATE-----", "privateKey": "-----BEGIN PRIVATE KEY-----\n<ATTACKERKEYPEM>\n-----END PRIVATE KEY-----", "replaces": [{"id": <VICTIMCERTID>}] }'

2. Observe victim.notify is now false and victim is queued for rotation curl -sS "<TARGETBASEURL>/api/1/certificates/<VICTIMCERTID>" \ -H "Authorization: Bearer <AUTHTOKEN>" | jq '.notify, .replaced'

3. On the next certificaterotate Celery beat tick, the attacker cert is deployed to every endpoint that was serving <VICTIMCERTID>.

Static-trace validation command from the source report:

bash 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 (run083, 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(ownerrole, 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 auditlog entry whenever a certificate is marked as replaced.

Affected Software

1 affected componentFixes available
pip/lemur>=0.5.0<=1.9.2
1.9.3

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/lemur to a version that resolves this vulnerability.

    Fixed in 1.9.3
  2. Configuration

    Before persisting `replaces`/`replacements` on certificate create, upload, and edit, iterate each resolved certificate referenced by `replaces` and apply the same `CertificatePermission(owner_role, cert.roles)` check used by the revoke endpoint; if the caller is not creator/owner/role-member/admin for any target certificate, reject the request with HTTP 403 (no partial assignment).

    lemur/certificates/views.py (POST /certificates and POST /certificates/upload/create/edit path handling replaces) CertificatePermission enforcement on replaces/replacements payload = Enforce CertificatePermission for each referenced certificate before assigning replaces; reject with 403 if caller is not creator/owner/role-member/admin for any target
  3. Configuration

    Change the SQLAlchemy `@event.listens_for(Certificate.replaces, 'append')` logic so it does not mutate the victim by setting `victim.notify = False` as a collection-append side effect; perform notification/auto-reissue suppression only after authorization checks succeed, to prevent an authorization failure from leaving the victim certificate partially mutated.

    lemur/certificates/models.py (SQLAlchemy Certificate.replaces append listener) value.notify side effect on append listener = Move `value.notify = False` side effect out of the `Certificate.replaces` append listener
  4. Configuration

    Whenever `Certificate.replaces` results in a certificate being marked as replaced, emit an `audit_log` entry (and ensure this is tied to successful, authorized replacement operations).

    lemur/certificates service (audit logging on replacement) audit_log emission when certificate is marked as replaced = Emit an `audit_log` entry whenever a certificate is marked as replaced
  5. Compensating control

    Immediately disable expiration notifications and auto-reissue for the production certificates that were marked as replaced (victim certificates with `victim.notify` observed false / queued for rotation).

  6. Operational

    On the next `certificate_rotate` Celery tick/run, verify endpoint certificate rotation behavior: ensure the attacker-controlled certificate is NOT pushed to any endpoints serving the affected victim certificates; remediate by correcting/clearing the `replaced`/`replaces` relationships so only the legitimate certificate remains deployed.

Event History

Aug 18, 2026
Advisory Published
via GitHub·08:51 PM
Data Sourced
via GitHub·08:51 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Who can exploit this issue?

Any authenticated user whose account is not read-only can exploit the affected certificate create, upload, or update routes. The user does not need ownership of, or CertificatePermission on, the certificate they reference, and can affect load balancers and Kubernetes secrets they do not otherwise have a role on.

2

What access and actions are required for exploitation?

The attacker must be able to authenticate as a non-read-only user and submit a replaces or replacements array containing a target certificate through POST /api/1/certificates, POST /api/1/certificates/upload, or PUT /api/1/certificates/<id>. No user interaction is required.

3

What operational impact indicates that a certificate may have been abused?

The affected certificate is marked with notify set to false and receives a replacement relationship. This suppresses its expiration notifications and automatic reissue, while the periodic certificate_rotate Celery task can deploy the attacker's replacement certificate to endpoints serving the affected certificate.

4

How can defenders identify potential exploitation?

Review certificate creation, upload, and update activity for replacements references to certificates the acting user did not own or have CertificatePermission to manage. Also investigate certificates with disabled notifications or unexpected replacement relationships, particularly where deployed endpoint certificates changed unexpectedly.

5

What should teams do if they cannot patch immediately?

Update to the release containing the referenced fix, v1.9.3. Until patching is possible, restrict access to non-read-only certificate-management accounts and closely review or prevent use of the replaces or replacements fields on the affected routes.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203