
PoC — l'import de pièces jointes copie des fichiers depuis des chemins locaux non approuvés dans ZotLit (GHSA-4qh7-66xv-h329, CVE-2026-87000, CVSS 5.5).
Statut CVE : demandé, en attente d'attribution. Cette découverte est publiée sous GHSA-4qh7-66xv-h329. À l'attribution de la CVE, ce dépôt est renommé
CVE-YYYY-NNNNN-zotlit-PoCet cette bannière est remplacée par le lien CVE.
| Chercheur | Dostxodjayev Abdullox (@squeeze440) |
| Avis | GHSA-4qh7-66xv-h329 |
| CVSS 3.1 | 5.5 (Moyen) |
| Faiblesse | CWE-73, CWE-200 |
Résumé
Le contrôle externe du nom de fichier ou du chemin dans la fonctionnalité d'import de pièces jointes d'AidenLx ZotLit (aidenlx/zotlit) 1.1.12 permet à un attaquant qui contrôle une bibliothèque Zotero partagée/synchronisée de divulguer des fichiers locaux arbitraires du système de fichiers de la victime dans le coffre Obsidian de la victime via un chemin de pièce jointe linked_file forgé.
Produit
ZotLit — plugin Obsidian pour l'intégration Zotero (aidenlx/zotlit, id du plugin zotlit, version du manifeste 1.1.12)
Version testée
Commit Git 41e60aaa6178629b34f8a42d104e757594b72435 (2026-07-31)
CVSS v3.1 estimé
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N — 5.5 (Moyen)
Métriques non évidentes : AV:L — l'exploitation se produit lorsque le processus local Obsidian/zotlit de la victime traite des métadonnées d'élément Zotero fournies par l'attaquant (livrées via une bibliothèque partagée/synchronisée), la même convention utilisée pour les bugs de type « fichier malveillant traité par une application locale », même si la livraison elle-même peut se faire par le réseau (bibliothèque de groupe partagée, fichier d'export envoyé par e-mail). UI:R — la victime doit importer/synchroniser la bibliothèque malveillante dans Zotero et exécuter la fonctionnalité d'import de notes de ZotLit (ou l'intégration d'annotations/citations) sur une note référençant la pièce jointe forgée ; il s'agit d'un usage courant de la fonctionnalité centrale du plugin, activée par défaut (attachment.import vaut true par défaut), et non d'une action inhabituelle. I:N/A:N — cette découverte n'est qu'une primitive de lecture/copie ; aucune traversée de chemin côté destination n'est revendiquée (voir Détails pour une observation connexe mais non vérifiée).
Détails
ZotLit lit les métadonnées des pièces jointes directement depuis la base SQLite de Zotero (ou une bibliothèque synchronisée/partagée), y compris la colonne en texte libre itemAttachments.path, et lui fait entièrement confiance pour résoudre l'emplacement depuis lequel lire une pièce jointe « liée » :
packages/db/src/lib/zt-path.ts:62-63 — attachmentAbsPath(), cas "linked-absolute" :
case "linked-absolute":
return parsed.path;
Pour les lignes linkMode: 2 (linked_file) dont le path ne porte pas le placeholder de répertoire de base attachments:, parsed.path est la chaîne brute de la base de données renvoyée telle quelle comme chemin absolu du système de fichiers à lire — aucune liste blanche, aucune confinement au répertoire de données Zotero ou à tout emplacement approuvé par l'utilisateur.
packages/db/src/lib/context/zt-template-attach.ts:101-106 — attachmentFilename(), cas "linked-absolute" :
case "linked-absolute":
return basename(path.path);
Le nom de fichier utilisé pour construire la copie dans le coffre est dérivé de ce même chemin contrôlé par l'attaquant via basename(), de sorte que le nom de fichier de destination est influencé par l'attaquant mais sans séparateur de chemin (sûr contre la traversée sur cette branche).
apps/obsidian/src/services/note-import/note-parser.ts:403-430 — resolveEmbeddedImage() est invoqué lors de la conversion d'une annotation d'image intégrée d'une note de littérature Zotero en Markdown Obsidian (une action courante et par défaut de la fonctionnalité « Import Note » de ZotLit). Il résout sourcePath = attachmentAbsPath(attachment, ...) et appelle deps.resolveLink({ sourcePath, vaultName: \${attachment.key}-${filename}` })` — mettant en file d'attente une copie de n'importe quel chemin absolu choisi par l'attaquant vers le coffre.
apps/obsidian/src/services/attachment-import/service.ts:125-155 — AttachmentImportBatch.resolveLink() met en file d'attente { source: sourcePath, dest: <dossier de pièces jointes du coffre>/<key>-<filename> }, conditionné uniquement par le paramètre attachment.import, dont la valeur par défaut est true (apps/obsidian/src/services/settings/schema.ts:123).
apps/obsidian/src/lib/copy-attachments.ts:43-60 — copyAttachment() effectue la lecture+écriture réelle sans aucune validation de chemin : stat(source) puis copyFile(source, dest).
Effet net : un attaquant capable de faire entrer un élément Zotero linked_file (linkMode 2) dans la bibliothèque de la victime — par exemple une bibliothèque de groupe Zotero partagée, un export .rdf/.json/Better BibTeX que la victime importe, ou une bibliothèque synchronisée sur laquelle l'attaquant a un accès en écriture — peut définir le chemin de pièce jointe de cet élément vers n'importe quel fichier du disque de la victime (~/.ssh/id_rsa, stockages d'identifiants de navigateur, autres coffres, fichiers .env, etc.). Dès que la victime exécute l'import de notes de ZotLit (ou affiche/intègre cette annotation) avec le paramètre par défaut attachment.import: true, ZotLit copie silencieusement le contenu de ce fichier dans le coffre Obsidian de la victime sous un nom prévisible (<attachmentKey>-<basename>). Comme les coffres sont couramment synchronisés, commités dans git ou publiés, cela déplace des données que l'attaquant n'aurait jamais pu atteindre autrement vers un emplacement que l'attaquant (ou toute autre personne ayant accès à la cible de synchronisation) peut lire.
Observation connexe non vérifiée (ne fait pas partie du PoC de cette découverte) : la branche sœur "storage" de attachmentFilename() (zt-template-attach.ts:103-104) renvoie le suffixe de chemin brut sans l'appel basename() appliqué aux deux autres branches. La question de savoir si cela est exploitable de manière indépendante dépend de la façon dont le mécanisme de synchronisation de stockage de Zotero nomme les fichiers téléchargés localement, ce qui est hors de ce dépôt et n'a pas été vérifié — signalé ici uniquement comme une lacune de durcissement qu'il vaut la peine de combler par cohérence.
Preuve de concept
Vérification dynamique : les fonctions vulnérables exactes (parseAttachmentPath, attachmentAbsPath, attachmentFilename, copyAttachments/copyAttachment/writeCopy/destMatches, isErrno, reflink) ont été extraites verbatim (fichier:ligne cités ci-dessus, logique inchangée) depuis le code source livré et exécutées directement dans Node, car une installation complète hors ligne de l'espace de travail pnpm n'était pas disponible dans ce bac à sable (chaîne d'outils corepack/pnpm cassée hors ligne). La seule substitution a été de remplacer l'appel au logger LogTape par console.warn — purement cosmétique, aucun effet sur le flux de contrôle. Script : ~/engagements/zotlit/evidence/poc-verify.mjs.
Étapes :
~/engagements/zotlit/evidence/victim-disk/id_rsa (contenu factice, clairement marqué comme simulé).{ key: "EVILKEY1", path: "<path to victim's id_rsa>", linkMode: 2 } — exactement la forme que getAttachmentByKey renvoie depuis la base de données de Zotero.attachmentAbsPath() — a résolu la source comme le fichier de la victime, verbatim, sans validation.attachmentFilename() — a résolu id_rsa comme nom de base de destination d'apparence sûre.vaultName depuis note-parser.ts:427 (${key}-${filename}).copyAttachments() — résultat { copied: 1, skipped: 0, missing: 0 }.fake-vault/attachments/EVILKEY1-id_rsa — le contenu correspond octet pour octet au fichier original de la victime.Capture d'écran de l'exécution complète (commande + sortie) dans un vrai xterm : ../evidence/poc-run.png
Impact
Un attaquant capable d'influencer le contenu de la bibliothèque Zotero d'une victime (bibliothèque partagée/de groupe, export bibliographique importé, ou bibliothèque synchronisée) peut exfiltrer des fichiers locaux arbitraires que l'utilisateur OS de la victime peut lire — clés SSH, stockages d'identifiants, contenu d'autres coffres, secrets .env/config — vers le coffre Obsidian de la victime, sans invite ni confirmation, dès que la victime utilise la fonctionnalité centrale d'import de notes de ZotLit avec les paramètres par défaut. Les coffres sont couramment synchronisés (Obsidian Sync, git, dossiers cloud) ou publiés, ce qui transforme une primitive de lecture de fichier local en une exposition distante réaliste.
Faiblesses
Remédiation
Il est suggéré de confiner les sources de pièces jointes linked_file (linkMode 2) aux emplacements du système de fichiers que la victime a explicitement configurés ou approuvés (par exemple le baseAttachmentPath configuré de Zotero lui-même, ou le répertoire de données de Zotero), et d'inviter l'utilisateur avant de copier automatiquement une pièce jointe liée dont le chemin sort de toute racine attendue. En défense en profondeur, envisagez d'appliquer la même sanitisation par basename() déjà utilisée sur les branches linked-absolute/linked-base de attachmentFilename() à la branche "storage" également, afin qu'aucun chemin de code ne renvoie un fragment de nom de fichier non assaini.
Crédit
Dostxodjayev Abdullox (GitHub : squeeze440)
Canal de signalement
Le signalement privé de vulnérabilités est activé sur aidenlx/zotlit — signalez via un brouillon d'avis de sécurité GitHub (GHSA) sur le dépôt plutôt que via une issue publique.