Back to Blog
high SEVERITY6 min read

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published September 7, 2026•Reviewed September 7, 2026

Answer Summary

Sensitive Data Exposure (CWE-200) in a Zotero plugin's `selectedItems2documents()` function leaked private metadata to OpenAI and other external LLMs via `JSON.stringify(item.toJSON())`. The vulnerability in `src/modules/Meet/Zotero.ts:47` sent full item data including tags, notes, relations, and attachment paths. The fix explicitly constructs a minimal object with only `title`, `abstractNote`, and `date` fields before transmission, eliminating exposure of sensitive metadata while preserving core functionality for LLM-powered similarity search.

Vulnerability at a Glance

cweCWE-200
fixExplicit field whitelisting: `{title, abstractNote, date}` instead of full object serialization
riskAutomatic transmission of private Zotero metadata to external LLM APIs without consent
languageTypeScript
root cause`JSON.stringify(item.toJSON())` serialized complete item data including sensitive fields
vulnerabilitySensitive Data Exposure

Introduction

In a Zotero plugin designed to enhance research workflows with LLM-powered features, we discovered a high-severity sensitive data exposure in src/modules/Meet/Zotero.ts. The selectedItems2documents() function at line 47 was automatically extracting complete document metadata—including private notes, attachment paths, tags, and relations—and transmitting this data to external LLM APIs like OpenAI without explicit user confirmation per operation.

The vulnerable code used JSON.stringify(item.toJSON()) to serialize entire Zotero item objects. While convenient for development, this pattern captured far more than the bibliographic data needed for similarity search, creating a significant privacy risk for researchers who might have sensitive annotations, unpublished notes, or confidential file locations stored in their Zotero libraries.

The Vulnerability Explained

The Problematic Code Pattern

The vulnerability resided in the selectedItems2documents() function, specifically at line 47:

async function selectedItems2documents(key: string) {
  const docs = ZoteroPane.getSelectedItems().map((item: Zotero.Item) => {
    const text = JSON.stringify(item.toJSON());  // ← VULNERABLE: Line 47
    return new Document({
      pageContent: text.slice(0, 500),
      metadata: {
        // ...
      }
    });
  });
  // ... routing to external LLM via getRelatedText
}

The item.toJSON() method returns a complete serialization of a Zotero item, which includes:

Field Category Examples Risk Level
Core bibliographic title, abstract, date Low — intended for sharing
Personal annotations tags, notes, relations High — private research data
System metadata attachment paths, library IDs High — reveals file system structure
Sync metadata modification dates, sync states Medium — operational intelligence

This serialized data then flowed through getRelatedText to external LLM services when the plugin wasn't in local LLM mode, with no per-operation confirmation dialog.

How It Could Be Exploited

Consider a realistic scenario: A researcher uses this plugin to find related papers for their work-in-progress manuscript. They select items from their Zotero library, unaware that the plugin is sending not just titles and abstracts, but also:

  • Private reading notes they added to items: "notes": [{"note": "Discuss with Bob about potential collaboration with CompetitorCorp..."}]
  • File paths revealing organizational structure: "path": "file:///Users/researcher/AcmeCorp/SecretProject/..."
  • Tag relationships exposing research interests: "tags": [{"tag": "whistleblower-interview"}, {"tag": "unpublished-2024"}]

All of this data would be transmitted to OpenAI's API (or other configured LLM providers), stored in their logs, potentially used for model training, and subject to their data retention policies—without any explicit consent for this specific transmission.

Real-World Impact

For academic researchers, journalists, legal professionals, and corporate R&D teams using Zotero, this represents a critical privacy breach:

  • Unintentional disclosure of confidential research directions
  • Exposure of source identities through notes and tags
  • Revealing unpublished work through modification patterns
  • Compliance violations for GDPR, HIPAA, or institutional data handling policies

The Fix

The remediation replaces the dangerous broad serialization with explicit field selection, constructing only the minimal object needed for LLM similarity search:

async function selectedItems2documents(key: string) {
  const docs = ZoteroPane.getSelectedItems().map((item: Zotero.Item) => {
    // Only send the minimal, non-sensitive bibliographic fields needed for
    // similarity search to external LLM services, instead of the full
    // item.toJSON() payload (which can include tags, notes, relations,
    // attachment paths and other private metadata).
    const text = JSON.stringify({
      title: item.getField("title"),
      abstractNote: item.getField("abstractNote"),
      date: item.getField("date")
    });
    return new Document({
      pageContent: text.slice(0, 500),
      metadata: {
        // ...
      }
    });
  });
}

Before vs. After Comparison

Aspect Before (Vulnerable) After (Fixed)
Data fields All toJSON() output Explicit: title, abstractNote, date
Serialization JSON.stringify(item.toJSON()) JSON.stringify({title, abstractNote, date})
Sensitivity risk High — complete metadata exposure Low — only public bibliographic data
User control Implicit via mode setting Same, but with minimal data exposure

Why This Fix Works

The fix implements data minimization by design:

  1. Explicit field access via item.getField() ensures only intended fields are retrieved
  2. Object literal construction creates a new, clean object with no inherited properties
  3. No sensitive field access — the fix deliberately excludes tags, notes, relations, attachments, and other sensitive collections
  4. Preserved functionality — the three selected fields (title, abstractNote, date) provide sufficient context for LLM-powered similarity search

Key Takeaways

  • Never use item.toJSON() for external transmission in Zotero plugins—this method returns complete item data including private metadata
  • The selectedItems2documents() function now constructs explicit field objects with only title, abstractNote, and date before any LLM API calls
  • Data minimization must be implemented at the source, not just secured in transit—encryption doesn't fix over-collection
  • LLM integrations require privacy-by-design architecture with clear boundaries between local and cloud processing modes
  • Per-operation consent dialogs should disclose exactly which fields will be transmitted, not just generic "send to AI" prompts

How Orbis AppSec Detected This

Source: Zotero item data via ZoteroPane.getSelectedItems() in src/modules/Meet/Zotero.ts

Sink: JSON.stringify(item.toJSON()) at line 47, with resulting string flowing to external LLM APIs through getRelatedText

Missing control: No field-level filtering or data minimization before serialization; absence of user confirmation per transmission operation

CWE: CWE-200: Exposure of Sensitive Information to an Unauthorized Actor

Fix: Replaced JSON.stringify(item.toJSON()) with explicit construction of a minimal object containing only title, abstractNote, and date fields

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

This vulnerability illustrates a critical lesson for the LLM integration era: convenience methods like toJSON() are dangerous for external data transmission. The fix demonstrates that security and functionality can coexist—by explicitly selecting only necessary fields, the plugin maintains its core LLM-powered features while eliminating privacy risks.

For developers building research tools, academic plugins, or any software bridging local data with cloud AI services, this case underscores the importance of data minimization, explicit consent, and careful review of serialization patterns. Privacy cannot be an afterthought in AI-powered workflows.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #161

Related Articles

critical

Lampa Desktop Auto-Update Heuristic Bypass: Execution of Unverified

Lampa Desktop's auto-update mechanism downloaded JavaScript and CSS from `raw.githubusercontent.com` using only heuristic validation—file size thresholds and string pattern matching—that attackers could trivially satisfy. The fix introduces cryptographic integrity verification by cross-referencing Git blob hashes from the GitHub Contents API, ensuring downloaded code matches the repository's authoritative state before execution.

high

adm-zip 0.6.0 Preserves SUID Bits From ZIPs: CVE-2026-102282

The `adm-zip` dependency resolved to 0.6.0 in this project's dependency tree, a version affected by CVE-2026-102282: during extraction it applies the Unix permission bits stored in each ZIP entry's external file attributes verbatim, including the setuid (`04000`), setgid (`02000`), and sticky bits. An attacker who controls an archive passed to `extractAllTo()` or `extractEntryTo()` can therefore have the extractor create a setuid binary owned by whatever user the extraction process runs as. The

high

requestInput() Type Confusion: NaN and Object Bypass in JavaScript

The `requestInput()` utility function lacked validation on its `type` parameter and failed to handle `NaN` results from float conversions, creating a type confusion weakness. An attacker could supply malformed inputs that propagate unhandled `NaN` values or unexpected object types through the type system. The fix adds explicit guards against `NaN` type parameters and rejects non-primitive type values.

critical

No Rate Limit on /api/uploads/presign Enables DoS

The `/api/uploads/presign` endpoint accepted unlimited concurrent requests to generate storage presigned URLs, giving an attacker a free lever to exhaust storage-provider quotas and server resources. The fix adds an `express-rate-limit` middleware capping each client to 30 requests per minute on that route.

high

CVE-2026-54673: builder-util-runtime Leaks Auth Headers on Redirect

electron-updater and electron-builder rely on builder-util-runtime to fetch update manifests and artifacts over HTTP. A flaw in that shared HTTP executor allowed credential headers attached to the original update-feed request to be re-sent after a redirect, exposing them to any host the redirect pointed to. The project fixes this by upgrading builder-util-runtime to 9.7.0 and collapsing a duplicate, older copy of the package that electron-updater had pinned on its own.

high

image-size 1.2.1 DoS: Zero-Valued Dimensions in Image Buffer Parser

A high-severity denial-of-service vulnerability in image-size 1.2.1 allows attackers to crash Node.js services using malicious image buffers with zero-valued dimensions. The fix removes the vulnerable `queue` dependency and tightens dimension validation in version 2.0.3.