CVE-2026-71322 is a medium-severity (CVSS 4.3) CWE-862 vulnerability in lemur. A fix is available for lemur — see the affected versions and patch details below.
Lemur: Missing authorization check on POST /certificates/<id>/export for plugins with requires_key = False
Exploitation Status
No confirmed exploitation observed yet
- CISA’s own triage has not observed active exploitation or public proof-of-concept code for this CVE as of its last assessment.
Exploitation and automatability from CISA’s SSVC triage for CVE-2026-71322.
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
How urgent is this, really
CVE-2026-71322 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.
Where this sits among everything scored
Of 382,205 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.
Real-World Exposure
lemurReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
Description
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:
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:
- The structural defect is latent and silent - any future
requires_key = FalseExportPluginthat does readcert.private_keywill inherit the bypass with no test or code-review signal. - 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-jksplugin) 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_viewevent 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
ExportPluginauthor who setsrequires_key = Falsebecause 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 distantif-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:
# 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
-
Set up Lemur with default configuration. Create an admin user
adminand a non-admin userevewith theread-onlyrole (or any role without certificate permissions). -
As
admin, issue a certificate. Note itsid. -
As
eve, invoke export with thejava-truststore-jksplugin:
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"}
]
}
}'
-
Observe HTTP 200 with a base64-encoded JKS truststore in the response.
evehad no permission overadmin's certificate, yet successfully exported its public material. -
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.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | lemur | all versions | 1.9.3pip install --upgrade 'lemur==1.9.3' |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for lemur, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update lemur to 1.9.3 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-71322 is resolved across your whole dependency graph.
Workarounds
Put an independent control in front of the weakness: restrict the affected endpoint or interface to trusted networks, require an additional authentication factor or proxy-level check, and invalidate existing sessions and credentials in case the flaw has already been used.
Frequently Asked Questions
Is CVE-2026-71322 in your dependencies?
Find it across PyPI, including transitive dependencies.