GHSA-4h97-p9wq-chqj: Medium severity pip/lemur vulnerability
Summary The CertificateExport handler in lemur/certificates/views.py nests its entire ownership / CertificatePermission check inside an if plugin.requireskey: branch. When the selected export plugin advertises requireskey = False, the authorization check is skipped entirely and any authenticated user can invoke plugin.export(cert.body, cert.chain, cert.privatekey, options) against a certificate they do not own. The handler additionally writes a "keyview" 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.requireskey: if not cert.privatekey: return (..., 400) else: if g.currentuser != cert.user: ownerrole = roleservice.getbyname(cert.owner) permission = CertificatePermission(ownerrole, [x.name for x in cert.roles]) if not permission.can(): return (..., 403) logservice.create(g.currentuser, "keyview", certificate=cert) # always logged extension, passphrase, data = plugin.export( cert.body, cert.chain, cert.privatekey, options ) The authorization gate is structurally inside the if plugin.requireskey: block. With requireskey = False, control falls straight through to plugin.export(...) with no ownership check. The cert.privatekey 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 requireskey = False is JavaTruststoreExportPlugin (lemur/plugins/lemurjks/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 requireskey = False ExportPlugin that does read cert.privatekey will inherit the bypass with no test or code-review signal. 2. The unconditional logservice.create(..., "keyview", ...) 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 keyview 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 requireskey = 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.requireskey: block so it runs for every export call: python Authorization first, unconditionally. if g.currentuser != cert.user: ownerrole = roleservice.getbyname(cert.owner) permission = CertificatePermission(ownerrole, [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.requireskey: if not cert.privatekey: return (dict(message="Plugin requires a key but none is present."), 400) logservice.create(g.currentuser, "keyview", certificate=cert) # only when key actually accessed extension, passphrase, data = plugin.export( cert.body, cert.chain, cert.privatekey, options ) This makes the authorization gate independent of the plugin's requireskey flag and correctly scopes the keyview 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/<certid>/export \ -H "Authorization: Bearer <evejwt>" \ -H "Content-Type: application/json" \ -d '{ "plugin": { "slug": "java-truststore-jks", "pluginoptions": [ {"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 userid, logtype, certificateid, loggedat FROM logs WHERE certificateid = <certid> ORDER BY loggedat DESC LIMIT 1;" The log row shows logtype = 'keyview' 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 Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/lemurto a version that resolves this vulnerability.Fixed in 1.9.3 - Configuration
In `lemur/certificates/views.py` (around `certificates/views.py:1573` where `plugin.export(cert.body, cert.chain, cert.private_key, options)` is called), lift the entire ownership / `CertificatePermission` authorization gate so it executes unconditionally for all export calls. Specifically, move it out of the `if plugin.requires_key:` branch so an authenticated user cannot export a certificate when the selected `ExportPlugin` advertises `requires_key = False`.
Lemur (Certificate export authorization in lemur/certificates/views.py:1573) Authorization check scope = Run ownership / CertificatePermission check for every export call (not conditional on ExportPlugin.requires_key) - Configuration
In `lemur/certificates/views.py`, change the audit logging so the `key_view` event is not written unconditionally. Instead of always calling `log_service.create(g.current_user, "key_view", certificate=cert)`, only create the `key_view` audit log entry when the export path actually accesses `cert.private_key` (i.e., when key access occurs), preventing false-positive `key_view` records when exporting with `requires_key = False` plugins.
Lemur audit logging for key-view events key_view audit event emission condition = Emit `log_service.create(..., "key_view", ...)` only when the export actually involves private key access
Event History
Frequently Asked Questions
Who can exploit this issue?
Authenticated users are exposed if they can invoke certificate export with a plugin that advertises requires_key = False. In that case, the ownership and CertificatePermission checks are skipped, allowing export to be invoked for certificates the user does not own.
What does an attacker need to exploit it?
An attacker needs valid authentication and access to the certificate export handler, then must select an export plugin with requires_key set to False. No user interaction is required.
How can we investigate possible prior exploitation?
Review certificate export activity involving plugins that do not require a key, especially exports initiated by users who do not own the target certificate. The key_view audit event is not reliable evidence that a private key was accessed, because it is logged for every export call.