
PoC — a importação de anexos copia arquivos de caminhos locais não aprovados no ZotLit (GHSA-4qh7-66xv-h329, CVE-2026-87000, CVSS 5.5).
Status da CVE: solicitada, aguardando atribuição. Esta descoberta é publicada como GHSA-4qh7-66xv-h329. Após a atribuição da CVE, este repositório é renomeado
CVE-YYYY-NNNNN-zotlit-PoCe este banner é substituído pelo link da CVE.
| Pesquisador | Dostxodjayev Abdullox (@squeeze440) |
| Aviso | GHSA-4qh7-66xv-h329 |
| CVSS 3.1 | 5.5 (Médio) |
| Fraqueza | CWE-73, CWE-200 |
Resumo
O Controle Externo de Nome de Arquivo ou Caminho no recurso de importação de anexos do AidenLx ZotLit (aidenlx/zotlit) 1.1.12 permite que um atacante que controla uma biblioteca Zotero compartilhada/sincronizada divulgue arquivos locais arbitrários do sistema de arquivos da vítima para o cofre Obsidian da vítima por meio de um caminho de anexo linked_file manipulado.
Produto
ZotLit — plugin do Obsidian para integração com o Zotero (aidenlx/zotlit, id do plugin zotlit, versão do manifesto 1.1.12)
Versão Testada
Commit do Git 41e60aaa6178629b34f8a42d104e757594b72435 (2026-07-31)
CVSS v3.1 Estimado
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N — 5.5 (Médio)
Métricas não óbvias: AV:L — a exploração ocorre quando o processo local do Obsidian/zotlit da vítima processa metadados de itens do Zotero fornecidos pelo atacante (entregues por meio de uma biblioteca compartilhada/sincronizada), a mesma convenção usada para bugs de "arquivo malicioso processado por aplicativo local", embora a entrega em si possa ocorrer pela rede (biblioteca de grupo compartilhada, arquivo de exportação enviado por e-mail). UI:R — a vítima deve importar/sincronizar a biblioteca maliciosa no Zotero e executar o recurso de importação de notas do ZotLit (ou de incorporação de anotações/citações) em uma nota que faça referência ao anexo manipulado; isso é uso rotineiro da funcionalidade central do plugin, habilitada por padrão (attachment.import tem valor padrão true), não uma ação incomum. I:N/A:N — esta descoberta é apenas uma primitiva de leitura/cópia; nenhum path traversal no lado do destino é alegado (consulte Detalhes para uma observação relacionada, mas não verificada).
Detalhes
O ZotLit lê metadados de anexos diretamente do banco de dados SQLite do Zotero (ou de uma biblioteca sincronizada/compartilhada), incluindo a coluna de texto livre itemAttachments.path, e confia nela completamente ao resolver de onde ler um anexo "vinculado":
packages/db/src/lib/zt-path.ts:62-63 — attachmentAbsPath(), caso "linked-absolute":
case "linked-absolute":
return parsed.path;
Para linhas com linkMode: 2 (linked_file) cujo path não carrega o placeholder de diretório base attachments:, parsed.path é a string bruta do banco de dados retornada literalmente como o caminho absoluto do sistema de arquivos a ser lido — sem allowlist, sem confinamento ao diretório de dados do Zotero ou a qualquer local aprovado pelo usuário.
packages/db/src/lib/context/zt-template-attach.ts:101-106 — attachmentFilename(), caso "linked-absolute":
case "linked-absolute":
return basename(path.path);
O nome de arquivo usado para construir a cópia no cofre é derivado desse mesmo caminho controlado pelo atacante por meio de basename(), portanto o nome de arquivo de destino é influenciado pelo atacante, mas livre de separadores de caminho (seguro contra traversal neste ramo).
apps/obsidian/src/services/note-import/note-parser.ts:403-430 — resolveEmbeddedImage() é invocado durante a conversão de uma anotação de imagem incorporada de uma nota de literatura do Zotero em Markdown do Obsidian (uma ação rotineira e padrão do recurso "Import Note" do ZotLit). Ele resolve sourcePath = attachmentAbsPath(attachment, ...) e chama deps.resolveLink({ sourcePath, vaultName: \${attachment.key}-${filename}` })` — enfileirando uma cópia de qualquer caminho absoluto escolhido pelo atacante para dentro do cofre.
apps/obsidian/src/services/attachment-import/service.ts:125-155 — AttachmentImportBatch.resolveLink() enfileira { source: sourcePath, dest: <vault attachment folder>/<key>-<filename> }, condicionado apenas à configuração attachment.import, cujo padrão é true (apps/obsidian/src/services/settings/schema.ts:123).
apps/obsidian/src/lib/copy-attachments.ts:43-60 — copyAttachment() executa a leitura+escrita real sem nenhuma validação de caminho: stat(source) e depois copyFile(source, dest).
Efeito líquido: um atacante que consiga inserir um item do Zotero linked_file (linkMode 2) na biblioteca da vítima — por exemplo, uma biblioteca de grupo compartilhada do Zotero, uma exportação .rdf/.json/Better BibTeX que a vítima importa, ou uma biblioteca sincronizada na qual o atacante tem acesso de escrita — pode definir o caminho de anexo desse item para qualquer arquivo no disco da vítima (~/.ssh/id_rsa, armazenamentos de credenciais do navegador, outros cofres, arquivos .env, etc.). No momento em que a vítima executa a importação de notas do ZotLit (ou renderiza/incorpora essa anotação) com a configuração padrão attachment.import: true, o ZotLit copia silenciosamente o conteúdo desse arquivo para o cofre Obsidian da vítima sob um nome previsível (<attachmentKey>-<basename>). Como os cofres são rotineiramente sincronizados, commitados no git ou publicados, isso move dados que o atacante jamais conseguiria alcançar de outra forma para um local que o atacante (ou qualquer outra pessoa com acesso ao destino de sincronização) pode ler.
Observação relacionada, não verificada (não faz parte do PoC desta descoberta): o ramo irmão "storage" de attachmentFilename() (zt-template-attach.ts:103-104) retorna o sufixo bruto do caminho sem a chamada basename() aplicada aos outros dois ramos. Se isso é explorável de forma independente depende de como o próprio mecanismo de sincronização de armazenamento do Zotero nomeia os arquivos baixados localmente, o que está fora deste repositório e não foi verificado — sinalizado aqui apenas como uma lacuna de hardening que vale a pena fechar por consistência.
Prova de Conceito
Verificação dinâmica: as funções vulneráveis exatas (parseAttachmentPath, attachmentAbsPath, attachmentFilename, copyAttachments/copyAttachment/writeCopy/destMatches, isErrno, reflink) foram extraídas literalmente (com arquivo:linha citados acima, lógica inalterada) do código-fonte distribuído e executadas diretamente no Node, porque uma instalação completa offline do workspace pnpm não estava disponível neste sandbox (toolchain corepack/pnpm quebrado offline). A única substituição foi trocar a chamada do logger LogTape por console.warn — apenas cosmético, efeito zero no fluxo de controle. Script: ~/engagements/zotlit/evidence/poc-verify.mjs.
Passos:
~/engagements/zotlit/evidence/victim-disk/id_rsa (conteúdo fictício, claramente marcado como simulado).{ key: "EVILKEY1", path: "<path to victim's id_rsa>", linkMode: 2 } — exatamente o formato que getAttachmentByKey retorna do banco de dados do Zotero.attachmentAbsPath() real — resolveu a origem como o arquivo da vítima, literalmente, sem validação.attachmentFilename() real — resolveu id_rsa como o basename de destino aparentemente seguro.vaultName a partir de note-parser.ts:427 (${key}-${filename}).copyAttachments() real — resultado { copied: 1, skipped: 0, missing: 0 }.fake-vault/attachments/EVILKEY1-id_rsa — conteúdo byte a byte idêntico ao arquivo original da vítima.Captura de tela da execução completa (comando + saída) em um xterm real: ../evidence/poc-run.png
Impacto
Um atacante que consiga influenciar o conteúdo da biblioteca Zotero de uma vítima (biblioteca compartilhada/de grupo, exportação de bibliografia importada ou biblioteca sincronizada) pode exfiltrar arquivos locais arbitrários que o usuário do SO da vítima consegue ler — chaves SSH, armazenamentos de credenciais, conteúdos de outros cofres, segredos em .env/configurações — para o cofre Obsidian da vítima, sem nenhum prompt ou confirmação, no momento em que a vítima usa a funcionalidade central de importação de notas do ZotLit com as configurações padrão. Os cofres são comumente sincronizados (Obsidian Sync, git, pastas na nuvem) ou publicados, então isso converte uma primitiva de leitura de arquivo local em exposição remota realista.
Fraquezas
Remediação
Sugere-se confinar as origens de anexos linked_file (linkMode 2) a locais do sistema de arquivos que a vítima configurou ou aprovou explicitamente (por exemplo, o próprio baseAttachmentPath configurado do Zotero, ou o diretório de dados do Zotero), e solicitar confirmação ao usuário antes de copiar automaticamente um anexo vinculado cujo caminho esteja fora de qualquer raiz esperada. Como defesa em profundidade, considere aplicar a mesma sanitização apenas com basename() já usada nos ramos linked-absolute/linked-base de attachmentFilename() também ao ramo "storage", para que nenhum caminho de código retorne um fragmento de nome de arquivo não sanitizado.
Crédito
Dostxodjayev Abdullox (GitHub: squeeze440)
Canal de Relato
O relato privado de vulnerabilidades está habilitado em aidenlx/zotlit — relate por meio de um rascunho de Aviso de Segurança do GitHub (GHSA) no repositório, em vez de uma issue pública.