Skip to content
KitploitKITPLOIT
ToolsBlog
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
zotlit-PoC — PoC — attachment import copies files from unapproved local paths in ZotLit (GHSA-4qh7-66xv-h329, CVE-2026-87000, CVSS 5.5). | Kitploit
Tools/GitHubGitHub/squeeze440/zotlit-poc
Vulnerability AnalysisExploitationData ExfiltrationInformation GatheringSecurity VirtualizationPapers & Research
GitHubsqueeze440/zotlit-poc

zotlit-PoC

PoC — attachment import copies files from unapproved local paths in ZotLit (GHSA-4qh7-66xv-h329, CVE-2026-87000, CVSS 5.5).

View Repository
7 days agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

ZotLit: security advisory

CVE status: requested, pending assignment. This finding is published as GHSA-4qh7-66xv-h329. On CVE assignment this repository is renamed CVE-YYYY-NNNNN-zotlit-PoC and this banner is replaced with the CVE link.

ResearcherDostxodjayev Abdullox (@squeeze440)
AdvisoryGHSA-4qh7-66xv-h329
CVSS 3.15.5 (Medium)
WeaknessCWE-73, CWE-200

Summary

External Control of File Name or Path in the attachment-import feature in AidenLx ZotLit (aidenlx/zotlit) 1.1.12 allows an attacker who controls a shared/synced Zotero library to disclose arbitrary local files from the victim's filesystem into the victim's Obsidian vault via a crafted linked_file attachment path.

Product

ZotLit — Obsidian plugin for Zotero integration (aidenlx/zotlit, plugin id zotlit, manifest version 1.1.12)

Tested Version

Git commit 41e60aaa6178629b34f8a42d104e757594b72435 (2026-07-31)

Estimated CVSS v3.1

CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N — 5.5 (Medium)

Non-obvious metrics: AV:L — exploitation occurs when the victim's local Obsidian/zotlit process processes attacker-supplied Zotero item metadata (delivered via a shared/synced library), the same convention used for "malicious file processed by local app" bugs, even though delivery itself can happen over a network (shared group library, emailed export file). UI:R — the victim must import/sync the malicious library into Zotero and run ZotLit's note-import (or annotation/citation embed) feature on a note referencing the crafted attachment; this is routine use of the plugin's core, default-enabled functionality (attachment.import defaults to true), not an unusual action. I:N/A:N — this finding is a read/copy primitive only; no destination-side path traversal is claimed (see Details for a related but unverified observation).

Details

ZotLit reads attachment metadata straight from Zotero's SQLite database (or a synced/shared library), including the free-text itemAttachments.path column, and trusts it completely when resolving where to read a "linked" attachment from:

  • packages/db/src/lib/zt-path.ts:62-63 — attachmentAbsPath(), case "linked-absolute":

    root@kitploit:~
    case "linked-absolute":
      return parsed.path;
    

    For linkMode: 2 (linked_file) rows whose path doesn't carry the attachments: base-dir placeholder, parsed.path is the raw DB string returned verbatim as the absolute filesystem path to read — no allowlist, no confinement to the Zotero data directory or any user-approved location.

  • packages/db/src/lib/context/zt-template-attach.ts:101-106 — attachmentFilename(), case "linked-absolute":

    root@kitploit:~
    case "linked-absolute":
      return basename(path.path);
    

    The filename used to build the in-vault copy is derived from that same attacker-controlled path via basename(), so the destination filename is attacker-influenced but path-separator-free (traversal-safe on this branch).

  • apps/obsidian/src/services/note-import/note-parser.ts:403-430 — resolveEmbeddedImage() is invoked while converting a Zotero literature-note's embedded image annotation into Obsidian Markdown (a routine, default action of ZotLit's "Import Note" feature). It resolves sourcePath = attachmentAbsPath(attachment, ...) and calls deps.resolveLink({ sourcePath, vaultName: \${attachment.key}-${filename}` })` — queuing a copy of any attacker-chosen absolute path into the vault.

  • apps/obsidian/src/services/attachment-import/service.ts:125-155 — AttachmentImportBatch.resolveLink() queues { source: sourcePath, dest: <vault attachment folder>/<key>-<filename> }, gated only by the attachment.import setting, whose default is true (apps/obsidian/src/services/settings/schema.ts:123).

  • apps/obsidian/src/lib/copy-attachments.ts:43-60 — copyAttachment() performs the actual read+write with no path validation: stat(source) then copyFile(source, dest).

Net effect: an attacker who can get a linked_file (linkMode 2) Zotero item into the victim's library — e.g. a shared Zotero group library, a .rdf/.json/Better BibTeX export the victim imports, or a synced library the attacker has write access to — can set that item's attachment path to any file on the victim's disk (~/.ssh/id_rsa, browser credential stores, other vaults, .env files, etc.). The moment the victim runs ZotLit's note-import (or renders/embeds that annotation) with the default attachment.import: true setting, ZotLit silently copies that file's content into the victim's Obsidian vault under a predictable name (<attachmentKey>-<basename>). Because vaults are routinely synced, git-committed, or published, this moves data the attacker could never otherwise reach into a location the attacker (or anyone else with access to the sync target) can read.

Related, unverified observation (not part of this finding's PoC): the sibling "storage" branch of attachmentFilename() (zt-template-attach.ts:103-104) returns the raw path suffix without the basename() call applied to the other two branches. Whether this is independently exploitable depends on how Zotero's own storage-sync mechanism names locally-downloaded files, which is outside this repo and was not verified — flagged here only as a hardening gap worth closing for consistency.

Proof of Concept

Dynamic verification: the exact vulnerable functions (parseAttachmentPath, attachmentAbsPath, attachmentFilename, copyAttachments/copyAttachment/writeCopy/destMatches, isErrno, reflink) were extracted verbatim (file:line-cited above, logic unchanged) from the shipped source and executed directly in Node, because a full offline pnpm workspace install was not available in this sandbox (corepack/pnpm toolchain broken offline). The only substitution was swapping the LogTape logger call for console.warn — cosmetic only, zero effect on control flow. Script: ~/engagements/zotlit/evidence/poc-verify.mjs.

Steps:

  1. Simulated victim secret at ~/engagements/zotlit/evidence/victim-disk/id_rsa (dummy content, clearly marked simulated).
  2. Crafted attacker-controlled Zotero attachment row: { key: "EVILKEY1", path: "<path to victim's id_rsa>", linkMode: 2 } — exactly the shape getAttachmentByKey returns from Zotero's DB.
  3. Ran the real attachmentAbsPath() — resolved the source as the victim's file, verbatim, no validation.
  4. Ran the real attachmentFilename() — resolved id_rsa as the safe-looking destination basename.
  5. Reproduced the real vaultName construction from note-parser.ts:427 (${key}-${filename}).
  6. Ran the real copyAttachments() — result { copied: 1, skipped: 0, missing: 0 }.
  7. Read back fake-vault/attachments/EVILKEY1-id_rsa — content byte-for-byte matches the victim's original file.

Screenshot of the full run (command + output) in a real xterm: ../evidence/poc-run.png

Impact

An attacker who can influence a victim's Zotero library contents (shared/group library, imported bibliography export, or synced library) can exfiltrate arbitrary local files the victim's OS user can read — SSH keys, credential stores, other vault contents, .env/config secrets — into the victim's Obsidian vault, with no prompt or confirmation, the moment the victim uses ZotLit's core note-import functionality with default settings. Vaults are commonly synced (Obsidian Sync, git, cloud folders) or published, so this converts a local-file-read primitive into realistic remote exposure.

Weaknesses

  • CWE-73: External Control of File Name or Path
  • CWE-200: Exposure of Sensitive Information to an Unauthorized Actor

Remediation

Suggest confining linked_file (linkMode 2) attachment sources to filesystem locations the victim has explicitly configured or approved (e.g. Zotero's own configured baseAttachmentPath, or the Zotero data directory), and prompting the user before auto-copying a linked attachment whose path falls outside any expected root. As defense in depth, consider applying the same basename()-only sanitization already used on the linked-absolute/linked-base branches of attachmentFilename() to the "storage" branch too, so no code path returns an unsanitized filename fragment.

Credit

Dostxodjayev Abdullox (GitHub: squeeze440)

Reporting Channel

Private vulnerability reporting is enabled on aidenlx/zotlit — report via a GitHub Security Advisory (GHSA) draft on the repository rather than a public issue.

Download Tool