CVE-2026-71322: Lemur Missing Authorization Check On POST Vulnerability
MODERATECVE-2026-71322 is a MODERATE (CVSS 4.3) missing authorization check on post vulnerability (CWE-862: Missing Authorization) affecting Lemur libraries in the PyPI ecosystem. The issue involves missing authorization check on post /certificates/<id>/export for plugins with requires_key = false. Remediation is available by upgrading affected packages to version 1.9.3.
CVE-2026-71322 at a Glance
Security Impact
CVSS: 4.3
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:L/I:N/A:N).
How to Fix CVE-2026-71322
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-71322, 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-862: Missing Authorization, where the product does not perform an authorization check when an actor attempts to access a resource or perform an action. 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-71322 Affected Lemur Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
lemur
|
PyPI |
< 1.9.3
|
1.9.3
|
References
Frequently Asked Questions
What is CVE-2026-71322?
Which Lemur versions are affected?
What version fixes CVE-2026-71322?
Is lemur affected?
What is the CVSS score of CVE-2026-71322?
What CWE is associated with CVE-2026-71322?
How can I check PyPI dependencies for CVE-2026-71322?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-4h97-p9wq-chqj",
"modified" : "2026-08-18T20:51:42Z",
"published" : "2026-08-18T20:51:42Z",
"aliases" : [ "CVE-2026-71322" ],
"summary" : "Lemur: Missing authorization check on POST /certificates/<id>/export for plugins with requires_key = False",
"details" : "## Summary\n \nThe `CertificateExport` handler in `lemur/certificates/views.py` nests its entire ownership / `CertificatePermission` check inside an `if plugin.requires_key:` branch. When the selected export plugin advertises `requires_key = False`, the authorization check is skipped entirely and any authenticated user can invoke `plugin.export(cert.body, cert.chain, cert.private_key, options)` against a certificate they do not own. The handler additionally writes a `\"key_view\"` audit-log event for every call, regardless of whether the plugin actually accessed the private key, polluting the audit trail with false positives.\n \n## Root Cause\n \n`lemur/certificates/views.py:1573`:\n \n```python\nif plugin.requires_key:\n if not cert.private_key:\n return (..., 400)\n else:\n if g.current_user != cert.user:\n owner_role = role_service.get_by_name(cert.owner)\n permission = CertificatePermission(owner_role, [x.name for x in cert.roles])\n if not permission.can():\n return (..., 403)\n \nlog_service.create(g.current_user, \"key_view\", certificate=cert) # always logged\nextension, passphrase, data = plugin.export(\n cert.body, cert.chain, cert.private_key, options\n)\n```\n \nThe authorization gate is structurally inside the `if plugin.requires_key:` block. With `requires_key = False`, control falls straight through to `plugin.export(...)` with no ownership check. The `cert.private_key` is passed to the plugin regardless of the flag — the flag only describes what the plugin *advertises* it needs, not what it actually receives.\n \nThe only currently shipping `ExportPlugin` with `requires_key = False` is `JavaTruststoreExportPlugin` (`lemur/plugins/lemur_jks/plugin.py`), whose `export()` ignores the `key` argument and emits a public-only Java truststore. The present-day data exposure is therefore limited to public certificate material. The bug is nonetheless filed as a real authorization gap because:\n \n1. The structural defect is latent and silent - any future `requires_key = False` `ExportPlugin` that *does* read `cert.private_key` will inherit the bypass with no test or code-review signal.\n2. The unconditional `log_service.create(..., \"key_view\", ...)` call falsely records key-view events for callers who never viewed a key, weakening incident-response signal.\n \n## Affected Endpoints\n \n| Method | Path | Source |\n|---|---|---|\n| POST | /api/1/certificates/`<id>`/export | lemur/certificates/views.py:1573 |\n \n## Impact\n \nIn the current codebase:\n \n- Any authenticated user can mint a Java truststore (`java-truststore-jks` plugin) containing any certificate's public body and chain, without owning the certificate or holding a role with permission over it.\n- The audit log records a `key_view` event for the calling user against that certificate, despite no private key having been accessed. Defenders investigating apparent key-view events will encounter false positives that they cannot distinguish from genuine accesses.\n \nLatent risk:\n \n- A future `ExportPlugin` author who sets `requires_key = False` because their plugin can *operate* without a key (e.g., for a fall-back code path) but still uses the key when one is provided will silently leak private keys to any authenticated user. The same code review that approves the plugin will not flag this — the authorization invariant is held by a structurally distant `if`-branch in the view, not by the plugin itself.\n \n## Remediation\n \nLift the authorization check out of the `if plugin.requires_key:` block so it runs for every export call:\n \n```python\n# Authorization first, unconditionally.\nif g.current_user != cert.user:\n owner_role = role_service.get_by_name(cert.owner)\n permission = CertificatePermission(owner_role, [x.name for x in cert.roles])\n if not permission.can():\n return (dict(message=\"You are not authorized to export this certificate.\"), 403)\n \nif plugin.requires_key:\n if not cert.private_key:\n return (dict(message=\"Plugin requires a key but none is present.\"), 400)\n log_service.create(g.current_user, \"key_view\", certificate=cert) # only when key actually accessed\n \nextension, passphrase, data = plugin.export(\n cert.body, cert.chain, cert.private_key, options\n)\n```\n \nThis makes the authorization gate independent of the plugin's `requires_key` flag and correctly scopes the `key_view` audit event to calls that actually involve key access.\n \n## Steps to Reproduce\n \n1. Set up Lemur with default configuration. Create an admin user `admin` and a non-admin user `eve` with the `read-only` role (or any role without certificate permissions).\n \n2. As `admin`, issue a certificate. Note its `id`.\n \n3. As `eve`, invoke export with the `java-truststore-jks` plugin:\n````\n curl -X POST https://lemur.local/api/1/certificates/<cert_id>/export \\\n -H \"Authorization: Bearer <eve_jwt>\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"plugin\": {\n \"slug\": \"java-truststore-jks\",\n \"plugin_options\": [\n {\"name\": \"passphrase\", \"value\": \"test\"}\n ]\n }\n }'\n````\n \n4. Observe HTTP 200 with a base64-encoded JKS truststore in the response. `eve` had no permission over `admin`'s certificate, yet successfully exported its public material.\n \n5. Inspect the audit log table or `lemur logs list`:\n````\n psql lemur -c \"SELECT user_id, log_type, certificate_id, logged_at FROM logs\n WHERE certificate_id = <cert_id> ORDER BY logged_at DESC LIMIT 1;\"\n````\n The log row shows `log_type = 'key_view'` for `eve` against `admin`'s certificate, despite no private key actually being accessed by the truststore plugin - confirming the audit-log pollution facet of the bug.",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "PyPI",
"name" : "lemur"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "1.9.3"
} ]
} ]
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/Netflix/lemur/security/advisories/GHSA-4h97-p9wq-chqj"
}, {
"type" : "WEB",
"url" : "https://github.com/Netflix/lemur/commit/5683bbea8b10cce07f9a8abf1e4a7d3b2031c585"
}, {
"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-862" ],
"severity" : "MODERATE",
"github_reviewed" : true,
"github_reviewed_at" : "2026-08-18T20:51:42Z",
"nvd_published_at" : null
}
}