GHSA-qcqw-jwxc-2hqg — lemur
HIGHGHSA-qcqw-jwxc-2hqg is a high-severity (CVSS 8.8) CWE-863 vulnerability in lemur. A fix is available for lemur — see the affected versions and patch details below.
Lemur has an authorization bypass in StrictRolePermission / AuthorityCreatorPermission
Exploitation Status
No confirmed exploitation observed yet
- A successful exploit gives an attacker total control of the affected component, not partial access.
- CISA’s own triage has not observed active exploitation or public proof-of-concept code for this CVE as of its last assessment.
Exploitation and automatability from CISA’s SSVC triage for GHSA-qcqw-jwxc-2hqg.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
How urgent is this, really
GHSA-qcqw-jwxc-2hqg plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.
Where this sits among everything scored
Of 377,636 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.
Real-World Exposure
lemurReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
Description
Summary
StrictRolePermission and AuthorityCreatorPermission in lemur/auth/permissions.py call flask_principal.Permission.__init__() with zero Needs 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.
Flask-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).
A 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.
Root Cause
# lemur/auth/permissions.py
class AuthorityCreatorPermission(Permission):
def __init__(self):
requires_admin = current_app.config.get("ADMIN_ONLY_AUTHORITY_CREATION", False)
if requires_admin:
super().__init__(RoleNeed("admin"))
else:
super().__init__() # empty Need set
class StrictRolePermission(Permission):
def __init__(self):
strict_role_enforcement = current_app.config.get("LEMUR_STRICT_ROLE_ENFORCEMENT", False)
if strict_role_enforcement:
needs = [RoleNeed("admin"), RoleNeed("operator")]
super().__init__(*needs)
else:
super().__init__() # empty Need set
flask_principal.Permission.allows() (upstream, v0.4.0):
def allows(self, identity):
if self.needs and not self.needs.intersection(identity.provides):
return False
...
return True
When 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.
Affected Endpoints
Views whose only authorization check is StrictRolePermission().can() or AuthorityCreatorPermission().can():
| Method | Path | Source |
|---|---|---|
| POST | /api/1/authorities | lemur/authorities/views.py:231 |
| POST | /api/1/certificates/upload | lemur/certificates/views.py:651 |
| POST | /api/1/pending_certificates/<id>/upload | lemur/pending_certificates/views.py:545 |
| POST | /api/1/notifications | lemur/notifications/views.py:227 |
| PUT/DEL | /api/1/notifications/<id> | lemur/notifications/views.py:370,384 |
| POST | /api/1/domains | lemur/domains/views.py:131 |
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.
Note 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.
Impact
A user holding only the read-only role can:
- 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.
- Upload arbitrary certificates via
/certificates/upload, including attacker-supplied keys with destinations such as AWS push. - Create or modify notifications, which reach an SSRF sink (see related finding F3, Slack notification webhook).
- Mark arbitrary domains as
sensitive, manipulating Lemur's domain registry and the cert-issuance approval path.
Combined 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.
Remediation
The 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.
class AuthorityCreatorPermission(Permission):
def __init__(self):
requires_admin = current_app.config.get("ADMIN_ONLY_AUTHORITY_CREATION", True)
...
class StrictRolePermission(Permission):
def __init__(self):
strict_role_enforcement = current_app.config.get("LEMUR_STRICT_ROLE_ENFORCEMENT", True)
...
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.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | lemur | all versions | 1.9.1pip install --upgrade 'lemur==1.9.1' |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for lemur, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update lemur to 1.9.1 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-qcqw-jwxc-2hqg is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like GHSA-qcqw-jwxc-2hqg can be triaged on real exposure rather than presence alone.
Tailored to GHSA-qcqw-jwxc-2hqg. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-qcqw-jwxc-2hqg in your dependencies?
O3 Security finds GHSA-qcqw-jwxc-2hqg across PyPI dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.