{"id":"CVE-2026-48508","aliases":["GHSA-qcqw-jwxc-2hqg","PYSEC-2026-2588"],"url":"https://o3.security/vulnerability/CVE-2026-48508","summary":"Lemur: Authorization bypass in StrictRolePermission / AuthorityCreatorPermission","details":"## Summary\n\n`StrictRolePermission` and `AuthorityCreatorPermission` in `lemur/auth/permissions.py` call `flask_principal.Permission.__init__()` with zero `Need`s when their config flags are unset. Both flags defaulted to `False` in code prior to the fix, so this was the state of any Lemur install that hadn't explicitly opted in.\n\nFlask-Principal's `Permission.allows()` returns `True` whenever `self.needs` is empty. The `.can()` gate therefore passes for every authenticated identity, including the lowest-privilege role Lemur ships (`read-only`).\n\nA user holding only `read-only` can create root Certificate Authorities, create and edit notifications (an SSRF sink), create domain entries, and upload arbitrary certificates. These classes are the sole authorization check on those endpoints.\n\n## Root Cause\n\n```python\n# lemur/auth/permissions.py\nclass AuthorityCreatorPermission(Permission):\n    def __init__(self):\n        requires_admin = current_app.config.get(\"ADMIN_ONLY_AUTHORITY_CREATION\", False)\n        if requires_admin:\n            super().__init__(RoleNeed(\"admin\"))\n        else:\n            super().__init__()              # empty Need set\n\nclass StrictRolePermission(Permission):\n    def __init__(self):\n        strict_role_enforcement = current_app.config.get(\"LEMUR_STRICT_ROLE_ENFORCEMENT\", False)\n        if strict_role_enforcement:\n            needs = [RoleNeed(\"admin\"), RoleNeed(\"operator\")]\n            super().__init__(*needs)\n        else:\n            super().__init__()              # empty Need set\n```\n\n`flask_principal.Permission.allows()` (upstream, v0.4.0):\n\n```python\ndef allows(self, identity):\n    if self.needs and not self.needs.intersection(identity.provides):\n        return False\n    ...\n    return True\n```\n\nWhen `self.needs == set()`, the first guard is falsy and is skipped. `excludes` is also empty, so `allows()` returns `True` for any identity. `AuthenticatedResource` (the parent class for these views) sets `method_decorators = [login_required]`, which checks authentication only, no role. So the permission `.can()` is the only authorization layer in front of these endpoints.\n\n## Affected Endpoints\n\nViews whose only authorization check is `StrictRolePermission().can()` or `AuthorityCreatorPermission().can()`:\n\n| Method   | Path                                      | Source                                  |\n|----------|-------------------------------------------|-----------------------------------------|\n| POST     | /api/1/authorities                        | lemur/authorities/views.py:231          |\n| POST     | /api/1/certificates/upload                | lemur/certificates/views.py:651         |\n| POST     | /api/1/pending_certificates/\\<id>/upload  | lemur/pending_certificates/views.py:545 |\n| POST     | /api/1/notifications                      | lemur/notifications/views.py:227        |\n| PUT/DEL  | /api/1/notifications/\\<id>                | lemur/notifications/views.py:370,384    |\n| POST     | /api/1/domains                            | lemur/domains/views.py:131              |\n\n`POST /api/1/authorities` is gated by `AuthorityCreatorPermission().can() and StrictRolePermission().can()`; both have empty Needs by default. The other rows are gated by `StrictRolePermission().can()` alone.\n\nNote on scope: `PUT /api/1/authorities/<id>` also references `StrictRolePermission`, but its gate is `AuthorityPermission(...).can() and StrictRolePermission().can()`. `AuthorityPermission` is constructed with non-empty Needs (`RoleNeed(\"admin\")` plus authority-scoped needs), so that endpoint is not bypassable by a `read-only` user and is excluded from this report.\n\n## Impact\n\nA user holding only the `read-only` role can:\n\n1. Create root Certificate Authorities. CA cert and private key are persisted in Lemur's DB with the attacker as owner. Any internal trust store that pins Lemur-managed roots can now be signed against by the attacker.\n2. Upload arbitrary certificates via `/certificates/upload`, including attacker-supplied keys with destinations such as AWS push.\n3. Create or modify notifications, which reach an SSRF sink (see related finding F3, Slack notification webhook).\n4. Mark arbitrary domains as `sensitive`, manipulating Lemur's domain registry and the cert-issuance approval path.\n\nCombined with any low-privilege credential leak (phished employee, leaked SSO token) or insider access, this is a pivot from \"any authenticated identity\" to control of the PKI issuance plane.\n\n## Remediation\n\nThe config flag defaults for `ADMIN_ONLY_AUTHORITY_CREATION` and `LEMUR_STRICT_ROLE_ENFORCEMENT` were changed from `False` to `True`. Restrictive role enforcement is now active on all default installs without requiring explicit configuration.\n\n```python\nclass AuthorityCreatorPermission(Permission):\n    def __init__(self):\n        requires_admin = current_app.config.get(\"ADMIN_ONLY_AUTHORITY_CREATION\", True)\n        ...\n\nclass StrictRolePermission(Permission):\n    def __init__(self):\n        strict_role_enforcement = current_app.config.get(\"LEMUR_STRICT_ROLE_ENFORCEMENT\", True)\n        ...\n```\n\n**Note:** The opt-out branches (empty Needs) remain in the code. Operators who explicitly set `ADMIN_ONLY_AUTHORITY_CREATION = False` or `LEMUR_STRICT_ROLE_ENFORCEMENT = False` in their config will reintroduce the bypass. These flags should be left unset or set to `True`.","published":"2026-08-18T18:03:08.944Z","modified":"2026-10-28T02:30:53.751619296Z","cvss":{"score":8.8,"severity":"HIGH","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H"},"epss":{"score":0.00236,"percentile":0.14775,"asOf":"2026-09-16"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"PyPI","name":"lemur","fixedVersion":"1.9.1"}],"fix":{"url":"https://github.com/Netflix/lemur/commit/e6a41e2c1b2028840d19039b06895524a19ad36f","label":"Netflix/lemur@e6a41e2"},"references":[{"type":"WEB","url":"https://github.com/Netflix/lemur/releases/tag/v1.9.1"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/48xxx/CVE-2026-48508.json"},{"type":"ADVISORY","url":"https://github.com/Netflix/lemur/security/advisories/GHSA-qcqw-jwxc-2hqg"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-48508"},{"type":"FIX","url":"https://github.com/Netflix/lemur/commit/e6a41e2c1b2028840d19039b06895524a19ad36f"},{"type":"PACKAGE","url":"https://github.com/Netflix/lemur"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-10-28T02:30:53.751619296Z"}}