CVE-2026-71308: Lemur Unchecked `replaces[]` Lets Any User Vulnerability
HIGHCVE-2026-71308 is a HIGH (CVSS 8.1) unchecked `replaces[]` lets any user vulnerability (CWE-639: Authorization Bypass Through User-Controlled Key) affecting Lemur libraries in the PyPI ecosystem. The issue involves unchecked `replaces[]` lets any user silence notifications and hijack auto-rotation for arbitrary certificates. Remediation is available by upgrading affected packages to version 1.9.3.
CVE-2026-71308 at a Glance
Security Impact
CVSS: 8.1
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H).
How to Fix CVE-2026-71308
To remediate this vulnerability, upgrade all affected Lemur dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
pip install lemur>=1.9.3
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
pip show lemur
How to Check if You Are Affected
To determine whether your Python environment is affected by CVE-2026-71308, inspect your installed packages using pip:
pip show lemur
Analyze the terminal output to locate any version of Lemur in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 1.9.3), your application is affected.
Note: The pip show lemur command checks currently installed packages in your active Python environment.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-639: Authorization Bypass Through User-Controlled Key, where the system's authorization functionality does not prevent one user from gaining access to another user's data or record by modifying the key value identifying the data. In Lemur, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-71308 Affected Lemur Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
lemur
|
PyPI |
≥ 0.5.0
< 1.9.3
|
1.9.3
|
References
Frequently Asked Questions
What is CVE-2026-71308?
Which Lemur versions are affected?
What version fixes CVE-2026-71308?
Is lemur affected?
What is the CVSS score of CVE-2026-71308?
What CWE is associated with CVE-2026-71308?
How can I check PyPI dependencies for CVE-2026-71308?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-cfh6-pv5c-38jv",
"modified" : "2026-08-18T20:51:33Z",
"published" : "2026-08-18T20:51:33Z",
"aliases" : [ "CVE-2026-71308" ],
"summary" : "Lemur: Unchecked `replaces[]` lets any user silence notifications and hijack auto-rotation for arbitrary certificates",
"details" : "## Summary\n\nRepo under test: https://github.com/Netflix/lemur\n\nThe 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.\n\nAny 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.\n\n## Affected route\n\n`POST /api/1/certificates`\n`POST /api/1/certificates/upload`\n`PUT /api/1/certificates/<id>`\n\n## Affected code\n\n- [`lemur/certificates/schemas.py:402`](https://github.com/Netflix/lemur/blob/main/lemur/certificates/schemas.py#L402) — `replaces = fields.Nested(AssociatedCertificateSchema, missing=[], many=True)` accepted on create/upload/edit\n- [`lemur/schemas.py:152`](https://github.com/Netflix/lemur/blob/main/lemur/schemas.py#L152) — `AssociatedCertificateSchema` resolves any certificate by id/name via `fetch_objects(Certificate, data)` with no permission check\n- [`lemur/certificates/views.py:651`](https://github.com/Netflix/lemur/blob/main/lemur/certificates/views.py#L651) — only `StrictRolePermission().can()` gates `/certificates/upload`; no check on `data['replaces']`\n- [`lemur/certificates/models.py:506`](https://github.com/Netflix/lemur/blob/main/lemur/certificates/models.py#L506) — `@event.listens_for(Certificate.replaces, 'append')` sets `value.notify = False` on the victim\n- [`lemur/certificates/service.py:277`](https://github.com/Netflix/lemur/blob/main/lemur/certificates/service.py#L277) — `get_all_pending_reissue()` filters `not_(Certificate.replaced.any())`, excluding the victim\n- [`lemur/certificates/cli.py:347`](https://github.com/Netflix/lemur/blob/main/lemur/certificates/cli.py#L347) — `request_rotation(endpoint, endpoint.certificate.replaced[0], message, commit)` deploys the attacker cert\n- [`lemur/common/celery.py:638`](https://github.com/Netflix/lemur/blob/main/lemur/common/celery.py#L638) — periodic `certificate_rotate` task runs `cli_certificate.rotate(..., commit=True)`\n- [`lemur/deployment/service.py:17`](https://github.com/Netflix/lemur/blob/main/lemur/deployment/service.py#L17) — `endpoint.source.plugin.update_endpoint(endpoint, new_cert)` pushes to ELB/CloudFront/ACM/K8s\n\n## Impact\n\nAn authenticated insider or holder of a stolen low-privilege token can, without holding any role on a target certificate:\n\n1. Upload a self-signed or attacker-minted certificate listing arbitrary high-value production certificate IDs in `replaces`.\n2. Immediately disable expiration notifications and auto-reissue for those production certificates.\n3. On the next scheduled `certificate_rotate` 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.\n\nMinimum 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.\n\n## Root cause\n\n`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.\n\n## Validated evidence\n\nStatic trace, confirmed by code inspection (validation status: `CONFIRMED`):\n\n- `replaces` is accepted in `CertificateInputSchema` / `CertificateUploadInputSchema` and resolved via `fetch_objects(Certificate, ...)` with no per-object authorization.\n- `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`.\n- The Celery `certificate_rotate` task and `cli.rotate()` consume `Endpoint.replaced.any()` unconditionally and deploy `replaced[0]` with `commit=True`.\n\n## Proof of concept / reproducer\n\nStatus: reconstructed from source report (static control-flow trace; not executed against a live CA).\n\nPreconditions: 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`.\n\n```bash\n# 1. Upload an attacker-controlled cert that \"replaces\" the victim\ncurl -sS -X POST \"<TARGET_BASE_URL>/api/1/certificates/upload\" \\\n -H \"Authorization: Bearer <AUTH_TOKEN>\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"attacker-replacement\",\n \"owner\": \"attacker@example.com\",\n \"body\": \"-----BEGIN CERTIFICATE-----\\n<ATTACKER_CERT_PEM>\\n-----END CERTIFICATE-----\",\n \"privateKey\": \"-----BEGIN PRIVATE KEY-----\\n<ATTACKER_KEY_PEM>\\n-----END PRIVATE KEY-----\",\n \"replaces\": [{\"id\": <VICTIM_CERT_ID>}]\n }'\n\n# 2. Observe victim.notify is now false and victim is queued for rotation\ncurl -sS \"<TARGET_BASE_URL>/api/1/certificates/<VICTIM_CERT_ID>\" \\\n -H \"Authorization: Bearer <AUTH_TOKEN>\" | jq '.notify, .replaced'\n\n# 3. On the next certificate_rotate Celery beat tick, the attacker cert is\n# deployed to every endpoint that was serving <VICTIM_CERT_ID>.\n```\n\nStatic-trace validation command from the source report:\n\n```bash\ngrep -n 'replaces' lemur/certificates/schemas.py lemur/certificates/views.py lemur/schemas.py \\\n && grep -n 'CertificatePermission' lemur/certificates/views.py\n```\n\nSource 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).\n\n## Suggested fix\n\nBefore 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`](https://github.com/Netflix/lemur/blob/main/lemur/certificates/views.py#L1677)); 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.",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "PyPI",
"name" : "lemur"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0.5.0"
}, {
"fixed" : "1.9.3"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 1.9.2"
}
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/Netflix/lemur/security/advisories/GHSA-cfh6-pv5c-38jv"
}, {
"type" : "WEB",
"url" : "https://github.com/Netflix/lemur/commit/286874535160952143b0afe2d356642669f9d4c6"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/Netflix/lemur"
}, {
"type" : "WEB",
"url" : "https://github.com/Netflix/lemur/releases/tag/v1.9.3"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-639", "CWE-862" ],
"severity" : "HIGH",
"github_reviewed" : true,
"github_reviewed_at" : "2026-08-18T20:51:33Z",
"nvd_published_at" : null
}
}