PT-2026-80219 · Pypi · Lemur

Published

2026-08-18

·

Updated

2026-08-18

CVSS v3.1

4.3

Medium

VectorAV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N

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

MethodPathSource
POST/api/1/certificates/<id>/exportlemur/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"}
        ]
       }
      }'
  1. 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.
  2. 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.

Fix

Missing Authorization

Found an issue in the description? Have something to add? Feel free to write us 👾

Weakness Enumeration

Related Identifiers

GHSA-4H97-P9WQ-CHQJ

Affected Products

Lemur