CVE-2026-71322: Lemur Missing Authorization Check On POST Vulnerability

MODERATE
Advisory Identifier: GHSA-4h97-p9wq-chqj
CVSS 4.3
CWE-862
Published
Advisory Summary

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

CVE Identifier CVE-2026-71322
GHSA Identifier GHSA-4h97-p9wq-chqj
Severity MODERATE
CVSS v3.1 Base Score 4.3
CWE Classification CWE-862: Missing Authorization
Affected Product Lemur
Package Ecosystem PyPI
Published Date
Last Updated

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).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction None (UI:N)
Scope Unchanged (S:U)
Confidentiality Low (C:L)
Integrity None (I:N)
Availability None (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.

lemur fixed: 1.9.3
pip install lemur>=1.9.3
Verify Direct and Transitive Dependencies

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

## Summary The `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. ## Root Cause `lemur/certificates/views.py:1573`: ```python if plugin.requires_key: if not cert.private_key: return (..., 400) else: if g.current_user != cert.user: owner_role = role_service.get_by_name(cert.owner) permission = CertificatePermission(owner_role, [x.name for x in cert.roles]) if not permission.can(): return (..., 403) log_service.create(g.current_user, "key_view", certificate=cert) # always logged extension, passphrase, data = plugin.export( cert.body, cert.chain, cert.private_key, options ) ``` The 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. The 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: 1. 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. 2. The unconditional `log_service.create(..., "key_view", ...)` call falsely records key-view events for callers who never viewed a key, weakening incident-response signal. ## Affected Endpoints | Method | Path | Source | |---|---|---| | POST | /api/1/certificates/`<id>`/export | lemur/certificates/views.py:1573 | ## Impact In the current codebase: - 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. - 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. Latent risk: - 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. ## Remediation Lift the authorization check out of the `if plugin.requires_key:` block so it runs for every export call: ```python # Authorization first, unconditionally. if g.current_user != cert.user: owner_role = role_service.get_by_name(cert.owner) permission = CertificatePermission(owner_role, [x.name for x in cert.roles]) if not permission.can(): return (dict(message="You are not authorized to export this certificate."), 403) if plugin.requires_key: if not cert.private_key: return (dict(message="Plugin requires a key but none is present."), 400) log_service.create(g.current_user, "key_view", certificate=cert) # only when key actually accessed extension, passphrase, data = plugin.export( cert.body, cert.chain, cert.private_key, options ) ``` This 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. ## Steps to Reproduce 1. 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). 2. As `admin`, issue a certificate. Note its `id`. 3. As `eve`, invoke export with the `java-truststore-jks` plugin: ```` curl -X POST https://lemur.local/api/1/certificates/<cert_id>/export \ -H "Authorization: Bearer <eve_jwt>" \ -H "Content-Type: application/json" \ -d '{ "plugin": { "slug": "java-truststore-jks", "plugin_options": [ {"name": "passphrase", "value": "test"} ] } }' ```` 4. 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. 5. Inspect the audit log table or `lemur logs list`: ```` psql lemur -c "SELECT user_id, log_type, certificate_id, logged_at FROM logs WHERE certificate_id = <cert_id> ORDER BY logged_at DESC LIMIT 1;" ```` 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.

CVE-2026-71322 Affected Lemur Versions

1 component(s)
Fixed Versions:
1.9.3
Product / Package Ecosystem Affected Versions Fixed Version
lemur
PyPI
< 1.9.3
1.9.3

References

Frequently Asked Questions

What is CVE-2026-71322?
CVE-2026-71322 is a security vulnerability in Lemur involving Missing authorization check on POST /certificates/<id>/export for plugins with requires_key = False under CWE-862: Missing Authorization.
Which Lemur versions are affected?
Affected versions include releases prior to 1.9.3 across affected packages: lemur.
What version fixes CVE-2026-71322?
Upgrading to 1.9.3 (or the latest patched release for your respective branch) resolves the vulnerability.
Is lemur affected?
Yes, lemur is affected in releases prior to 1.9.3. Upgrade your dependency to 1.9.3 or later.
What is the CVSS score of CVE-2026-71322?
CVE-2026-71322 has a CVSS v3.1 base score of 4.3 (MODERATE) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N.
What CWE is associated with CVE-2026-71322?
CVE-2026-71322 is classified under CWE-862: Missing Authorization (The product does not perform an authorization check when an actor attempts to access a resource or perform an action.).
How can I check PyPI dependencies for CVE-2026-71322?
Run the dependency tree command: 'pip show lemur'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "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
  }
}