PYSEC-2026-3677
Vulnerability from pysec - Published: 2026-08-19 11:56 - Updated: 2026-08-19 12:16Summary
Repo under test: https://github.com/Netflix/lemur
When ADMIN_ONLY_AUTHORITY_CREATION=False (an explicitly supported and documented configuration), POST /api/1/authorities with type=subca never verifies that the caller holds AuthorityPermission on the supplied parent authority. The parent field is resolved by AssociatedAuthoritySchema via a raw fetch_objects(Authority, data) lookup, then passed straight through service.create → mint → cryptography-issuer.create_authority, which loads options["parent"].authority_certificate.private_key and signs a brand-new intermediate CA on the caller's behalf.
Any authenticated non-read-only user can therefore mint a sub-CA chained to any internal root whose private key Lemur holds — including roots they hold no role on — attach a role they already belong to, and immediately issue or offline-sign trusted leaf certificates for arbitrary names.
Affected route
POST /api/1/authorities (with type=subca)
Affected code
lemur/authorities/views.py:231— gates only onAuthorityCreatorPermission()+StrictRolePermission(); noAuthorityPermissionondata['parent']lemur/authorities/schemas.py:58—parent = fields.Nested(AssociatedAuthoritySchema);validate_subcaonly checks presencelemur/schemas.py:107—AssociatedAuthoritySchema.get_object→fetch_objects(Authority, data)resolves any authority by id/name with no permission checklemur/plugins/lemur_cryptography/plugin.py:40—private_key = options["authority"].authority_certificate.private_key(set fromoptions["parent"]) signs the new intermediatelemur/auth/permissions.py:62—AuthorityCreatorPermissionbecomes always-allow whenADMIN_ONLY_AUTHORITY_CREATION=Falselemur/certificates/views.py:538—is_private_authority(true forcryptography-issuer) skipsUSER_DOMAIN_AUTHORIZATION_PROVIDERfor subsequent leaf issuance
Impact
In deployments that set ADMIN_ONLY_AUTHORITY_CREATION=False to enable self-service CA creation, any authenticated non-read-only user — with zero permission on a given internal root CA — can obtain a working intermediate CA chained to that root. They can then:
- Issue TLS certificates for arbitrary names trusted by every relying party that trusts the internal root, bypassing
LEMUR_ALLOWED_DOMAINS, sensitive-domain flags, and the per-user domain-authorization plugin. - Export the sub-CA private key and sign end-entity certificates entirely outside Lemur, defeating all in-product issuance controls.
This converts "can create a self-contained test CA" into "can mint trusted certs under any internal PKI root in the organisation". The ADMIN_ONLY_AUTHORITY_CREATION documentation does not warn operators of this consequence.
Root cause
AuthoritiesList.post evaluates AuthorityCreatorPermission (a global "may create authorities" flag) and StrictRolePermission, but never evaluates AuthorityPermission(parent.id, parent.roles) against the caller-supplied parent. AssociatedAuthoritySchema is a pure lookup schema with no authz hook, and neither authorities.service.create nor mint re-check before invoking issuer_plugin.create_authority(options). The bundled cryptography-issuer then uses the parent's stored private key directly.
Validated evidence
Static trace, confirmed by code inspection (validation status: CONFIRMED):
parentis loaded viaAssociatedAuthoritySchema(rawfetch_objects), passed unchecked throughviews.post → service.create → mint → plugin.create_authority → issue_certificate, where the parent authority's stored private key is read and used to sign the new intermediate.- No call to
AuthorityPermission(parent.id, ...)exists anywhere on this path. - Precondition
ADMIN_ONLY_AUTHORITY_CREATION=Falseis an explicitly supported config (docs/administration.rst:517).
Proof of concept / reproducer
Status: reconstructed from source report (static control-flow trace; not executed against a live CA).
Preconditions: ADMIN_ONLY_AUTHORITY_CREATION=False; attacker is an authenticated Lemur user holding any role other than read-only; <PARENT_AUTHORITY_ID> is any internal cryptography-issuer root CA the attacker holds no role on; <ATTACKER_ROLE> is any role the attacker already belongs to.
curl -sS -X POST "<TARGET_BASE_URL>/api/1/authorities" \
-H "Authorization: Bearer <AUTH_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"name": "attacker-subca",
"owner": "attacker@example.com",
"description": "poc",
"type": "subca",
"parent": {"id": <PARENT_AUTHORITY_ID>},
"plugin": {"slug": "cryptography-issuer"},
"roles": [{"name": "<ATTACKER_ROLE>"}],
"commonName": "attacker-intermediate",
"validityYears": 1
}'
The response contains a new authority whose authority_certificate is signed by <PARENT_AUTHORITY_ID>'s private key. The caller is recorded as creator and holds <ATTACKER_ROLE> on it, so POST /api/1/certificates against the new authority succeeds (and skips allowed_issuance_for_domain because is_private_authority is true).
Static-trace validation command from the source report:
grep -n 'parent' lemur/authorities/schemas.py lemur/authorities/views.py lemur/authorities/service.py \
&& sed -n '37,55p' lemur/plugins/lemur_cryptography/plugin.py
Source artifact: audit/harnesses/public-repo-threat-model-harness/results/netflix-lemur-100run-mythos-20260627T051129Z/findings.jsonl (run_022, finding cluster lemur-subca-parent-authz, 5/100 runs).
Suggested fix
In AuthoritiesList.post (or authorities.service.create), when data.get('parent') is present, enforce AuthorityPermission(parent.id, [r.name for r in parent.roles]).can() before invoking the issuer plugin, regardless of ADMIN_ONLY_AUTHORITY_CREATION. Additionally, update the ADMIN_ONLY_AUTHORITY_CREATION documentation to state that disabling it currently grants every authenticated user the ability to chain sub-CAs off any internal root whose private key Lemur holds. Consider also requiring admin (or an explicit per-parent capability) for any type=subca creation independent of the global flag.
| Name | purl | lemur | pkg:pypi/lemur |
|---|
{
"affected": [
{
"package": {
"ecosystem": "PyPI",
"name": "lemur",
"purl": "pkg:pypi/lemur"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.9.3"
}
],
"type": "ECOSYSTEM"
}
],
"versions": [
"0.11.0",
"0.2.1",
"0.8.0",
"0.8.1",
"0.9.0",
"1.0.0",
"1.1.0",
"1.2.0",
"1.3.1",
"1.3.2",
"1.4.0",
"1.5.0",
"1.6.0",
"1.7.0",
"1.8.0",
"1.8.1",
"1.8.2",
"1.9.0",
"1.9.1",
"1.9.2"
]
}
],
"aliases": [
"CVE-2026-71317",
"GHSA-g7p5-89mh-248h"
],
"details": "## Summary\n\nRepo under test: https://github.com/Netflix/lemur\n\nWhen `ADMIN_ONLY_AUTHORITY_CREATION=False` (an explicitly supported and documented configuration), `POST /api/1/authorities` with `type=subca` never verifies that the caller holds `AuthorityPermission` on the supplied `parent` authority. The `parent` field is resolved by `AssociatedAuthoritySchema` via a raw `fetch_objects(Authority, data)` lookup, then passed straight through `service.create \u2192 mint \u2192 cryptography-issuer.create_authority`, which loads `options[\"parent\"].authority_certificate.private_key` and signs a brand-new intermediate CA on the caller\u0027s behalf.\n\nAny authenticated non-read-only user can therefore mint a sub-CA chained to any internal root whose private key Lemur holds \u2014 including roots they hold no role on \u2014 attach a role they already belong to, and immediately issue or offline-sign trusted leaf certificates for arbitrary names.\n\n## Affected route\n\n`POST /api/1/authorities` (with `type=subca`)\n\n## Affected code\n\n- [`lemur/authorities/views.py:231`](https://github.com/Netflix/lemur/blob/main/lemur/authorities/views.py#L231) \u2014 gates only on `AuthorityCreatorPermission()` + `StrictRolePermission()`; no `AuthorityPermission` on `data[\u0027parent\u0027]`\n- [`lemur/authorities/schemas.py:58`](https://github.com/Netflix/lemur/blob/main/lemur/authorities/schemas.py#L58) \u2014 `parent = fields.Nested(AssociatedAuthoritySchema)`; `validate_subca` only checks presence\n- [`lemur/schemas.py:107`](https://github.com/Netflix/lemur/blob/main/lemur/schemas.py#L107) \u2014 `AssociatedAuthoritySchema.get_object` \u2192 `fetch_objects(Authority, data)` resolves any authority by id/name with no permission check\n- [`lemur/plugins/lemur_cryptography/plugin.py:40`](https://github.com/Netflix/lemur/blob/main/lemur/plugins/lemur_cryptography/plugin.py#L40) \u2014 `private_key = options[\"authority\"].authority_certificate.private_key` (set from `options[\"parent\"]`) signs the new intermediate\n- [`lemur/auth/permissions.py:62`](https://github.com/Netflix/lemur/blob/main/lemur/auth/permissions.py#L62) \u2014 `AuthorityCreatorPermission` becomes always-allow when `ADMIN_ONLY_AUTHORITY_CREATION=False`\n- [`lemur/certificates/views.py:538`](https://github.com/Netflix/lemur/blob/main/lemur/certificates/views.py#L538) \u2014 `is_private_authority` (true for `cryptography-issuer`) skips `USER_DOMAIN_AUTHORIZATION_PROVIDER` for subsequent leaf issuance\n\n## Impact\n\nIn deployments that set `ADMIN_ONLY_AUTHORITY_CREATION=False` to enable self-service CA creation, any authenticated non-read-only user \u2014 with zero permission on a given internal root CA \u2014 can obtain a working intermediate CA chained to that root. They can then:\n\n- Issue TLS certificates for arbitrary names trusted by every relying party that trusts the internal root, bypassing `LEMUR_ALLOWED_DOMAINS`, sensitive-domain flags, and the per-user domain-authorization plugin.\n- Export the sub-CA private key and sign end-entity certificates entirely outside Lemur, defeating all in-product issuance controls.\n\nThis converts \"can create a self-contained test CA\" into \"can mint trusted certs under any internal PKI root in the organisation\". The `ADMIN_ONLY_AUTHORITY_CREATION` documentation does not warn operators of this consequence.\n\n## Root cause\n\n`AuthoritiesList.post` evaluates `AuthorityCreatorPermission` (a global \"may create authorities\" flag) and `StrictRolePermission`, but never evaluates `AuthorityPermission(parent.id, parent.roles)` against the caller-supplied `parent`. `AssociatedAuthoritySchema` is a pure lookup schema with no authz hook, and neither `authorities.service.create` nor `mint` re-check before invoking `issuer_plugin.create_authority(options)`. The bundled `cryptography-issuer` then uses the parent\u0027s stored private key directly.\n\n## Validated evidence\n\nStatic trace, confirmed by code inspection (validation status: `CONFIRMED`):\n\n- `parent` is loaded via `AssociatedAuthoritySchema` (raw `fetch_objects`), passed unchecked through `views.post \u2192 service.create \u2192 mint \u2192 plugin.create_authority \u2192 issue_certificate`, where the parent authority\u0027s stored private key is read and used to sign the new intermediate.\n- No call to `AuthorityPermission(parent.id, ...)` exists anywhere on this path.\n- Precondition `ADMIN_ONLY_AUTHORITY_CREATION=False` is an explicitly supported config ([`docs/administration.rst:517`](https://github.com/Netflix/lemur/blob/main/docs/administration.rst#L517)).\n\n## Proof of concept / reproducer\n\nStatus: reconstructed from source report (static control-flow trace; not executed against a live CA).\n\nPreconditions: `ADMIN_ONLY_AUTHORITY_CREATION=False`; attacker is an authenticated Lemur user holding any role other than `read-only`; `\u003cPARENT_AUTHORITY_ID\u003e` is any internal `cryptography-issuer` root CA the attacker holds no role on; `\u003cATTACKER_ROLE\u003e` is any role the attacker already belongs to.\n\n```bash\ncurl -sS -X POST \"\u003cTARGET_BASE_URL\u003e/api/1/authorities\" \\\n -H \"Authorization: Bearer \u003cAUTH_TOKEN\u003e\" \\\n -H \"Content-Type: application/json\" \\\n -d \u0027{\n \"name\": \"attacker-subca\",\n \"owner\": \"attacker@example.com\",\n \"description\": \"poc\",\n \"type\": \"subca\",\n \"parent\": {\"id\": \u003cPARENT_AUTHORITY_ID\u003e},\n \"plugin\": {\"slug\": \"cryptography-issuer\"},\n \"roles\": [{\"name\": \"\u003cATTACKER_ROLE\u003e\"}],\n \"commonName\": \"attacker-intermediate\",\n \"validityYears\": 1\n }\u0027\n```\n\nThe response contains a new authority whose `authority_certificate` is signed by `\u003cPARENT_AUTHORITY_ID\u003e`\u0027s private key. The caller is recorded as creator and holds `\u003cATTACKER_ROLE\u003e` on it, so `POST /api/1/certificates` against the new authority succeeds (and skips `allowed_issuance_for_domain` because `is_private_authority` is true).\n\nStatic-trace validation command from the source report:\n\n```bash\ngrep -n \u0027parent\u0027 lemur/authorities/schemas.py lemur/authorities/views.py lemur/authorities/service.py \\\n \u0026\u0026 sed -n \u002737,55p\u0027 lemur/plugins/lemur_cryptography/plugin.py\n```\n\nSource artifact: `audit/harnesses/public-repo-threat-model-harness/results/netflix-lemur-100run-mythos-20260627T051129Z/findings.jsonl` (run_022, finding cluster `lemur-subca-parent-authz`, 5/100 runs).\n\n## Suggested fix\n\nIn `AuthoritiesList.post` (or `authorities.service.create`), when `data.get(\u0027parent\u0027)` is present, enforce `AuthorityPermission(parent.id, [r.name for r in parent.roles]).can()` before invoking the issuer plugin, regardless of `ADMIN_ONLY_AUTHORITY_CREATION`. Additionally, update the `ADMIN_ONLY_AUTHORITY_CREATION` documentation to state that disabling it currently grants every authenticated user the ability to chain sub-CAs off any internal root whose private key Lemur holds. Consider also requiring admin (or an explicit per-parent capability) for any `type=subca` creation independent of the global flag.",
"id": "PYSEC-2026-3677",
"modified": "2026-08-19T12:16:27.512565Z",
"published": "2026-08-19T11:56:28.516318Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/Netflix/lemur/security/advisories/GHSA-g7p5-89mh-248h"
},
{
"type": "WEB",
"url": "https://github.com/Netflix/lemur/commit/8669011203ca3dd89d9e39bab9ef6850eca723f9"
},
{
"type": "PACKAGE",
"url": "https://github.com/Netflix/lemur"
},
{
"type": "WEB",
"url": "https://github.com/Netflix/lemur/releases/tag/v1.9.3"
},
{
"type": "PACKAGE",
"url": "https://pypi.org/project/lemur"
},
{
"type": "ADVISORY",
"url": "https://github.com/advisories/GHSA-g7p5-89mh-248h"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-71317"
}
],
"severity": [
{
"score": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "Lemur: Sub-CA creation never checks `AuthorityPermission` on the parent authority"
}
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.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.