
The Certificate Row Is Not the Certificate: A Revocation Bypass in Lemur
A newly disclosed Lemur flaw lets a low-privileged user create a duplicate certificate record and use it to revoke the real certificate at its issuing CA. The problem is a subtle but serious authorization mistake: trusting local row ownership instead of the CA-side certificate identity.
A GitHub Security Advisory published yesterday describes a nasty authorization flaw in Netflix Lemur: an authenticated non-read-only user can potentially revoke certificates they do not own by creating a duplicate Lemur record first.
That is not a minor data-integrity issue. Certificate revocation is an operational kill switch. Used against a production certificate, it can turn into a TLS outage for an endpoint that the attacker was never supposed to control.
The advisory, GHSA-pxmc-2ffp-8j67, is a good example of why authorization checks need to protect the thing that matters, not merely the application object closest to the API route.
In this case, Lemur checks whether a user can revoke a database row. But the CA revokes a certificate identity. Those are not necessarily the same thing.
That gap is the vulnerability.
The short version
Lemur exposes a revoke route at PUT /api/1/certificates/<id>/revoke. Its authorization logic is based on the selected Lemur Certificate row: the caller is permitted if they created that row or have the relevant CertificatePermission through its roles.
Separately, the upload route at POST /api/1/certificates/upload allows a user who passes StrictRolePermission to create a certificate row with attacker-supplied certificate metadata. That includes the certificate body, the issuing authority, and an external ID.
The problem is that Lemur does not require those values to be unique. It also does not enforce AuthorityPermission when resolving the supplied authority.
So an attacker can take public certificate data from a certificate they can read, upload a second row that points at the same CA-side certificate, and become the owner of that duplicate row. When they revoke their row, the configured CA plugin uses the duplicated certificate body or external ID to revoke the real certificate at the issuing CA.
The local record is fake. The revocation is not.
A permission check on an alias is not a permission check on the resource being acted on.
Why this is worse than a duplicate-record bug
It is tempting to describe this as "users can upload duplicate certificates." That is true, but it undersells the issue.
Duplicate data becomes security-critical when another action treats that data as an authority-bearing reference.
The report traces the flow through several pieces of Lemur:
CertificateUploadInputSchemaaccepts a caller-providedauthorityandexternal_id.AssociatedAuthoritySchemaresolves an authority by ID or name without a permission check.- The upload service attaches the supplied authority to the new certificate record.
- The model makes
nameunique, but not the certificate body, serial, or external ID. Name collisions are simply suffixed. - The revoke endpoint allows the row creator through without requiring
CertificatePermission. - The endpoint safety check examines
cert.endpointson that one row. - CA plugins then revoke based on the certificate body or external ID stored on the selected row.
That combination matters. No single line needs to look obviously catastrophic in isolation.
A permissive upload route might be intentional. Letting users upload externally issued certificates might be intentional. Letting a record creator manage their own record might be intentional. Avoiding a revoke operation for a certificate attached to an endpoint is obviously intentional.
Put together, though, those decisions create a path around every meaningful control.
The authorization boundary is in the wrong place
The way I see it: this is a classic confused-identity problem.
Lemur has two identities in play:
- The Lemur row identity: an internal database record with an owner, roles, and endpoint associations.
- The CA-side certificate identity: the certificate that an issuer recognizes and can revoke, identified by values such as its body, serial, or provider-specific external ID.
The revoke endpoint authorizes against the first identity. The CA plugin performs the irreversible external action against the second.
That only works if one Lemur row always maps cleanly to one CA-side certificate, and if untrusted users cannot manufacture aliases for an existing CA-side identity.
According to the advisory, neither condition holds.
For ACME, the plugin revokes using certificate.body. For DigiCert, Entrust, Google CA, and CFSSL paths described in the report, revocation uses certificate.external_id. If a duplicate row carries the same body or external ID and points to the same authority, it becomes another handle for the same external certificate.
Internally, Lemur sees a record owned by the attacker. Externally, the CA sees a request to revoke the victim's certificate, authenticated with the authority's stored CA credentials.
That last part is important. The attacker does not need direct CA credentials. They are abusing Lemur as a privileged revocation proxy.
The endpoint safeguard can be bypassed too
Lemur includes a sensible-looking safeguard: do not revoke a certificate while it is attached to an endpoint.
But that safeguard checks the endpoints relationship on the row being revoked.
A duplicate row has no endpoint associations. The production row may be attached to live endpoints, but the duplicate is not. The check therefore passes while the CA-side action still targets the same certificate.
This is exactly why authorization and safety checks cannot be evaluated only against an application-level reference when the actual operation is performed on a shared external identity.
Consider the broader pattern:
- A cloud inventory tool lets users create duplicate records for an account, key, bucket, or instance.
- It authorizes destructive actions based on record ownership.
- Its backend then invokes a provider API using the underlying provider identifier.
That is the same class of bug. The object in your database is merely a pointer. If users can create pointers freely, ownership of a pointer is meaningless unless it is tied to ownership of the target.
Why certificate visibility makes the path practical
The advisory also notes that CertificateOutputSchema exposes the certificate body, external ID, and authority to authenticated users.
The certificate body itself is public material, so its exposure is not inherently a secret leak. But the security question is not whether each field is confidential. It is whether the collection of visible fields is sufficient to invoke a privileged action.
Here, it is.
A user who can enumerate certificates can obtain the identifiers required to create an alias. The report describes a fleet-wide impact: a low-privileged insider, or someone holding a stolen non-admin token or API key, could iterate through readable certificate records and target them through duplicate rows.
That changes the risk from an isolated authorization mistake into a potential mass-denial-of-service path.
Revocation is particularly unforgiving because the blast radius often extends beyond the application that triggered it. A certificate may be deployed across multiple TLS endpoints, renewal jobs may need to be repaired, and downstream consumers may continue to fail until replacement certificates are issued and installed.
The attacker does not need to exfiltrate a private key. They only need the ability to make the trusted management plane revoke the certificate for them.
What the fix needs to do
The advisory's suggested fixes point in the right direction, but the core principle is more useful than any individual patch:
Authorize revocation against every local representation of the CA-side certificate identity, or make duplicate representations impossible.
At a minimum, teams operating Lemur should assess four controls.
First, upload should require AuthorityPermission when a caller supplies an authority. A user should not be able to bind a new record to an authority just because they know its ID or name.
Second, caller-supplied external_id should be rejected or ignored during upload unless there is a tightly controlled, authorized import workflow. External IDs are not harmless metadata when a plugin uses them as revocation handles.
Third, before calling plugin.revoke_certificate, Lemur should find every certificate row representing the same CA-side certificate. The report suggests matching by (authority_id, serial) or by body. Permission checks and endpoint-attached checks need to apply across that whole equivalence set, not only the row named in the request.
Finally, enforce uniqueness or deduplication for a stable certificate identity, such as (authority_id, serial). The exact key depends on the issuer integration and data model, but the goal is straightforward: do not allow two user-controlled rows to silently refer to the same revocable certificate.
There is also a disclosure issue. Non-owners should not receive external_id if it can function as a provider-side action token. Teams often classify fields as secret or non-secret. That is too simple. A value can be public enough to display and still be dangerous because it is an input to a privileged backend workflow.
What I would check immediately
If you operate Lemur or have built similar internal certificate tooling, start with the data model and follow the action all the way to the issuer.
Ask these questions:
- Can a user create a second local object that references an existing external resource?
- Does any destructive route authorize against the local object rather than the external resource identity?
- Are provider identifiers accepted from client input?
- Does a safety check inspect only one local record even though several records can represent the same real-world resource?
- Can users read identifiers that become valid inputs to privileged provider API calls?
- Are import, upload, and migration paths held to the same authorization standard as normal create flows?
Do not stop at route middleware. Trace the values passed into the external plugin or SDK call. That is where the real authorization boundary becomes visible.
The source report was validated by static control-flow analysis and code inspection, not by revoking a live CA certificate. That restraint matters. A live proof would be destructive, and the code path described is already enough to establish why this deserves urgent review.
This is not primarily a certificate-management lesson. It is a systems-design lesson. When an internal record can alias an external resource, the authorization model has to follow the external resource. Otherwise, somebody will eventually create a record they are allowed to control and use it to control something they are not.
Related posts
- Security
How I got free cinema credit by ordering -2 popcorns
A missing input validation on M-Tix Cinema XXI's food ordering API let me increase my account balance by submitting negative quantities. No tools needed — just a browser.
May 19, 2026 · 6 min - Security
How I analyze API security headers in 30 seconds
A quick checklist for reading HTTP response headers and spotting security misconfigurations before you even look at the response body.
May 18, 2026 · 7 min - Security
Common auth mistakes I find when reverse-engineering APIs
After years of poking at APIs that weren't meant to be poked at, these are the auth patterns that break most often — and why.
May 18, 2026 · 9 min