CVE-2026-71303
Lemur: Incomplete fix for CVE-2026-55166 -- ACME authority update endpoint allows non-admin to replace `acme_url` with internal IP, bypassing allowlist
Lemur manages TLS certificate creation. Prior to 1.9.3, _validate_acme_url enforced ACME_DIRECTORY_HOST_ALLOWLIST when an authority was created, but PUT /api/1/authorities/ passed options to lemur/authorities/service.py without applying the same check. A user holding an authority role could replace the stored acme_url with an internal service or instance-metadata URL such as 169.254.169.254. The next issuance operation loaded that value and passed it to ClientV2.get_directory, causing an outbound request from the Lemur backend. This bypassed the creation-time mitigation for CVE-2026-55166 and could expose internal services or cloud metadata. The fix revalidates acme_url whenever authority options are updated. 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
- High
- Integrity
- None
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
7.7
Lemur: Incomplete fix for CVE-2026-55166 -- ACME authority update endpoint allows non-admin to replace `acme_url` with internal IP, bypassing allowlist
Lemur manages TLS certificate creation. Prior to 1.9.3, _validate_acme_url enforced ACME_DIRECTORY_HOST_ALLOWLIST when an authority was created, but PUT /api/1/authorities/ passed options to lemur/authorities/service.py without applying the same check. A user holding an authority role could replace the stored acme_url with an internal service or instance-metadata URL such as 169.254.169.254. The next issuance operation loaded that value and passed it to ClientV2.get_directory, causing an outbound request from the Lemur backend. This bypassed the creation-time mitigation for CVE-2026-55166 and could expose internal services or cloud metadata. The fix revalidates acme_url whenever authority options are updated. This issue is fixed in version 1.9.3.
- Record
CVE-2026-71303- CVSS 3.1
- 7.7 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N - Weakness
- CWE-918
- Updated
- 2026-11-02
EUVDEUVD-2026-61154
7.7
Lemur manages TLS certificate creation. Prior to 1.9.3, _validate_acme_url enforced ACME_DIRECTORY_HOST_ALLOWLIST when an authority was created, but PUT /api/1/authorities/ passed options to lemur/authorities/service.py without applying the same check. A user holding an authority role could replace the stored acme_url with an internal service or instance-metadata URL such as 169.254.169.254. The next issuance operation loaded that value and passed it to ClientV2.get_directory, causing an outbound request from the Lemur backend. This bypassed the creation-time mitigation for CVE-2026-55166 and could expose internal services or cloud metadata. The fix revalidates acme_url whenever authority options are updated. This issue is fixed in version 1.9.3.
- Record
EUVD-2026-61154- CVSS 3.1
- 7.7 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-v5rc-cpwc-cfpr
7.7
Lemur: Incomplete fix for GHSA-v2wp-frmc-5q3v -- ACME authority update endpoint allows non-admin to replace `acme_url` with internal IP, bypassing allowlist
Summary
The fix for GHSA-v2wp-frmc-5q3v added _validate_acme_url() to reject acme_url values not in ACME_DIRECTORY_HOST_ALLOWLIST, but the validation is only called at authority creation time (POST). The authority update endpoint (PUT /api/1/authorities/<id>) accepts and stores arbitrary options -- including a modified acme_url -- without invoking the allowlist check. Any user with an authority role (granted by an admin to allow issuing certificates via that authority) can therefore overwrite the stored acme_url with an internal IP or IMDS endpoint. The next certificate issuance via that authority causes Lemur's backend to fetch the attacker-controlled URL, achieving SSRF.
Details
Where the fix lives (POST path -- protected):
lemur/plugins/lemur_acme/plugin.py lines 333-337 (ACMEIssuerPlugin.create_authority):
for option in plugin_options:
if option.get("name") == "certificate":
acme_root = option.get("value")
if option.get("name") == "acme_url":
_validate_acme_url(option.get("value", "")) # allowlist enforced
_validate_acme_url at line 35:
def _validate_acme_url(url):
"""Reject acme_url values that are not in the configured allowlist.
Called at authority creation time only -- existing authorities in the DB
were already trusted when they were created and are not re-validated.
"""
allowed_hosts = current_app.config.get(
"ACME_DIRECTORY_HOST_ALLOWLIST",
{"acme-v02.api.letsencrypt.org", ...},
)
parsed = urlparse(url)
if parsed.scheme != "https" or parsed.hostname not in allowed_hosts:
raise InvalidConfiguration(...)
Where the gap is (PUT path -- unprotected):
lemur/authorities/views.py lines 405-424 (Authorities.put):
authority = service.get(authority_id)
roles = [x.name for x in authority.roles]
permission = AuthorityPermission(authority_id, roles)
if not permission.can() or not StrictRolePermission().can():
return dict(message="You are not authorized to update this authority."), 403
return service.update(
authority_id,
owner=data["owner"],
description=data["description"],
active=data["active"],
roles=data["roles"],
options=data.get("options") # stored verbatim -- no ACME URL check
)
lemur/authorities/service.py lines 28-46 (update):
def update(authority_id, description, owner, active, roles, options=None):
authority = get(authority_id)
authority.roles = roles
authority.active = active
authority.description = description
authority.owner = owner
if options:
authority.options = options # written to DB with no _validate_acme_url call
return database.update(authority)
Where the SSRF sink is:
lemur/plugins/lemur_acme/acme_handlers.py lines 157-188:
for option in json.loads(authority.options):
options[option["name"]] = option.get("value")
directory_url = options.get("acme_url", current_app.config.get("ACME_DIRECTORY_URL"))
...
directory = ClientV2.get_directory(directory_url, net) # outbound HTTP to stored URL
With the default configuration (LEMUR_STRICT_ROLE_ENFORCEMENT = False, reverted in 1.9.2 per the GHSA-qcqw-jwxc-2hqg correction), StrictRolePermission().can() passes for any non-read-only user. Any user granted membership in an authority's role group by an admin can therefore call PUT /api/1/authorities/<id> to overwrite acme_url with an arbitrary URL. The allowlist enforced at creation is silently discarded.
PoC
Prerequisites:
- Lemur 1.9.2, default config (
LEMUR_STRICT_ROLE_ENFORCEMENTnot set, defaults toFalse) - Admin grants non-admin user membership in an ACME authority's role (normal operational step to allow certificate issuance)
- Attacker has a valid Lemur session token
Step 1 -- Authenticate as the non-admin user (role: TestRootCA_operator):
POST /api/1/auth/login HTTP/1.1
Host:…
- Record
GHSA-v5rc-cpwc-cfpr- CVSS 3.1
- 7.7 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N - Weakness
- CWE-918
- Updated
- 2026-11-02
NVD
7.7
Lemur manages TLS certificate creation. Prior to 1.9.3, _validate_acme_url enforced ACME_DIRECTORY_HOST_ALLOWLIST when an authority was created, but PUT /api/1/authorities/ passed options to lemur/authorities/service.py without applying the same check. A user holding an authority role could replace the stored acme_url with an internal service or instance-metadata URL such as 169.254.169.254. The next issuance operation loaded that value and passed it to ClientV2.get_directory, causing an outbound request from the Lemur backend. This bypassed the creation-time mitigation for CVE-2026-55166 and could expose internal services or cloud metadata. The fix revalidates acme_url whenever authority options are updated. This issue is fixed in version 1.9.3.
- Record
CVE-2026-71303- CVSS 3.1
- 7.7 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N - Weakness
- CWE-918
- Updated
- 2026-11-02
OSV
7.7
Lemur: Incomplete fix for CVE-2026-55166 -- ACME authority update endpoint allows non-admin to replace `acme_url` with internal IP, bypassing allowlist
Lemur manages TLS certificate creation. Prior to 1.9.3, _validate_acme_url enforced ACME_DIRECTORY_HOST_ALLOWLIST when an authority was created, but PUT /api/1/authorities/ passed options to lemur/authorities/service.py without applying the same check. A user holding an authority role could replace the stored acme_url with an internal service or instance-metadata URL such as 169.254.169.254. The next issuance operation loaded that value and passed it to ClientV2.get_directory, causing an outbound request from the Lemur backend. This bypassed the creation-time mitigation for CVE-2026-55166 and could expose internal services or cloud metadata. The fix revalidates acme_url whenever authority options are updated. This issue is fixed in version 1.9.3.
- Record
CVE-2026-71303- CVSS 3.1
- 7.7 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N - Weakness
- CWE-918
- Updated
- 2026-11-02
OSVGHSA-v5rc-cpwc-cfpr
7.7
Lemur: Incomplete fix for GHSA-v2wp-frmc-5q3v -- ACME authority update endpoint allows non-admin to replace `acme_url` with internal IP, bypassing allowlist
Summary
The fix for GHSA-v2wp-frmc-5q3v added _validate_acme_url() to reject acme_url values not in ACME_DIRECTORY_HOST_ALLOWLIST, but the validation is only called at authority creation time (POST). The authority update endpoint (PUT /api/1/authorities/<id>) accepts and stores arbitrary options -- including a modified acme_url -- without invoking the allowlist check. Any user with an authority role (granted by an admin to allow issuing certificates via that authority) can therefore overwrite the stored acme_url with an internal IP or IMDS endpoint. The next certificate issuance via that authority causes Lemur's backend to fetch the attacker-controlled URL, achieving SSRF.
Details
Where the fix lives (POST path -- protected):
lemur/plugins/lemur_acme/plugin.py lines 333-337 (ACMEIssuerPlugin.create_authority):
for option in plugin_options:
if option.get("name") == "certificate":
acme_root = option.get("value")
if option.get("name") == "acme_url":
_validate_acme_url(option.get("value", "")) # allowlist enforced
_validate_acme_url at line 35:
def _validate_acme_url(url):
"""Reject acme_url values that are not in the configured allowlist.
Called at authority creation time only -- existing authorities in the DB
were already trusted when they were created and are not re-validated.
"""
allowed_hosts = current_app.config.get(
"ACME_DIRECTORY_HOST_ALLOWLIST",
{"acme-v02.api.letsencrypt.org", ...},
)
parsed = urlparse(url)
if parsed.scheme != "https" or parsed.hostname not in allowed_hosts:
raise InvalidConfiguration(...)
Where the gap is (PUT path -- unprotected):
lemur/authorities/views.py lines 405-424 (Authorities.put):
authority = service.get(authority_id)
roles = [x.name for x in authority.roles]
permission = AuthorityPermission(authority_id, roles)
if not permission.can() or not StrictRolePermission().can():
return dict(message="You are not authorized to update this authority."), 403
return service.update(
authority_id,
owner=data["owner"],
description=data["description"],
active=data["active"],
roles=data["roles"],
options=data.get("options") # stored verbatim -- no ACME URL check
)
lemur/authorities/service.py lines 28-46 (update):
def update(authority_id, description, owner, active, roles, options=None):
authority = get(authority_id)
authority.roles = roles
authority.active = active
authority.description = description
authority.owner = owner
if options:
authority.options = options # written to DB with no _validate_acme_url call
return database.update(authority)
Where the SSRF sink is:
lemur/plugins/lemur_acme/acme_handlers.py lines 157-188:
for option in json.loads(authority.options):
options[option["name"]] = option.get("value")
directory_url = options.get("acme_url", current_app.config.get("ACME_DIRECTORY_URL"))
...
directory = ClientV2.get_directory(directory_url, net) # outbound HTTP to stored URL
With the default configuration (LEMUR_STRICT_ROLE_ENFORCEMENT = False, reverted in 1.9.2 per the GHSA-qcqw-jwxc-2hqg correction), StrictRolePermission().can() passes for any non-read-only user. Any user granted membership in an authority's role group by an admin can therefore call PUT /api/1/authorities/<id> to overwrite acme_url with an arbitrary URL. The allowlist enforced at creation is silently discarded.
PoC
Prerequisites:
- Lemur 1.9.2, default config (
LEMUR_STRICT_ROLE_ENFORCEMENTnot set, defaults toFalse) - Admin grants non-admin user membership in an ACME authority's role (normal operational step to allow certificate issuance)
- Attacker has a valid Lemur session token
Step 1 -- Authenticate as the non-admin user (role: TestRootCA_operator):
POST /api/1/auth/login HTTP/1.1
Host:…
- Record
GHSA-v5rc-cpwc-cfpr- CVSS 3.1
- 7.7 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N - Weakness
- CWE-918
- Updated
- 2026-11-02
OSVPYSEC-2026-3679
7.7
Lemur: Incomplete fix for GHSA-v2wp-frmc-5q3v -- ACME authority update endpoint allows non-admin to replace `acme_url` with internal IP, bypassing allowlist
Summary
The fix for GHSA-v2wp-frmc-5q3v added _validate_acme_url() to reject acme_url values not in ACME_DIRECTORY_HOST_ALLOWLIST, but the validation is only called at authority creation time (POST). The authority update endpoint (PUT /api/1/authorities/<id>) accepts and stores arbitrary options -- including a modified acme_url -- without invoking the allowlist check. Any user with an authority role (granted by an admin to allow issuing certificates via that authority) can therefore overwrite the stored acme_url with an internal IP or IMDS endpoint. The next certificate issuance via that authority causes Lemur's backend to fetch the attacker-controlled URL, achieving SSRF.
Details
Where the fix lives (POST path -- protected):
lemur/plugins/lemur_acme/plugin.py lines 333-337 (ACMEIssuerPlugin.create_authority):
for option in plugin_options:
if option.get("name") == "certificate":
acme_root = option.get("value")
if option.get("name") == "acme_url":
_validate_acme_url(option.get("value", "")) # allowlist enforced
_validate_acme_url at line 35:
def _validate_acme_url(url):
"""Reject acme_url values that are not in the configured allowlist.
Called at authority creation time only -- existing authorities in the DB
were already trusted when they were created and are not re-validated.
"""
allowed_hosts = current_app.config.get(
"ACME_DIRECTORY_HOST_ALLOWLIST",
{"acme-v02.api.letsencrypt.org", ...},
)
parsed = urlparse(url)
if parsed.scheme != "https" or parsed.hostname not in allowed_hosts:
raise InvalidConfiguration(...)
Where the gap is (PUT path -- unprotected):
lemur/authorities/views.py lines 405-424 (Authorities.put):
authority = service.get(authority_id)
roles = [x.name for x in authority.roles]
permission = AuthorityPermission(authority_id, roles)
if not permission.can() or not StrictRolePermission().can():
return dict(message="You are not authorized to update this authority."), 403
return service.update(
authority_id,
owner=data["owner"],
description=data["description"],
active=data["active"],
roles=data["roles"],
options=data.get("options") # stored verbatim -- no ACME URL check
)
lemur/authorities/service.py lines 28-46 (update):
def update(authority_id, description, owner, active, roles, options=None):
authority = get(authority_id)
authority.roles = roles
authority.active = active
authority.description = description
authority.owner = owner
if options:
authority.options = options # written to DB with no _validate_acme_url call
return database.update(authority)
Where the SSRF sink is:
lemur/plugins/lemur_acme/acme_handlers.py lines 157-188:
for option in json.loads(authority.options):
options[option["name"]] = option.get("value")
directory_url = options.get("acme_url", current_app.config.get("ACME_DIRECTORY_URL"))
...
directory = ClientV2.get_directory(directory_url, net) # outbound HTTP to stored URL
With the default configuration (LEMUR_STRICT_ROLE_ENFORCEMENT = False, reverted in 1.9.2 per the GHSA-qcqw-jwxc-2hqg correction), StrictRolePermission().can() passes for any non-read-only user. Any user granted membership in an authority's role group by an admin can therefore call PUT /api/1/authorities/<id> to overwrite acme_url with an arbitrary URL. The allowlist enforced at creation is silently discarded.
PoC
Prerequisites:
- Lemur 1.9.2, default config (
LEMUR_STRICT_ROLE_ENFORCEMENTnot set, defaults toFalse) - Admin grants non-admin user membership in an ACME authority's role (normal operational step to allow certificate issuance)
- Attacker has a valid Lemur session token
Step 1 -- Authenticate as the non-admin user (role: TestRootCA_operator):
POST /api/1/auth/login HTTP/1.1
Host:…
- Record
PYSEC-2026-3679- CVSS 3.1
- 7.7 High
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N - Updated
- 2026-11-02
Affected products
Affected packages
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-71303 is a high vulnerability (CVSS 7.7) 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.28%, the 20th percentile. Fixed in lemur 1.9.3.
References
github.com/Netflix/lemur/security/advisories/GHSA-v5rc-cpwc-cfpr
github.com/Netflix/lemur/commit/edca0390f930344d65ff4ca37a669c2320e3dfad
github.com/Netflix/lemur/releases/tag/v1.9.3
github.com/advisories/GHSA-v5rc-cpwc-cfpr
github.com/CVEProject/cvelistV5/tree/main/cves/2026/71xxx/CVE-2026-71303.json
nvd.nist.gov/vuln/detail/CVE-2026-71303
github.com/Netflix/lemur
pypi.org/project/lemur
Left out here: the rest of 3 source descriptions. The JSON record has them.