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:
- Explicit field access via
item.getField()ensures only intended fields are retrieved - Object literal construction creates a new, clean object with no inherited properties
- No sensitive field access — the fix deliberately excludes
tags,notes,relations,attachments, and other sensitive collections - Preserved functionality — the three selected fields (
title,abstractNote,date) provide sufficient context for LLM-powered similarity search
Prevention & Best Practices
For Plugin Developers
-
Audit
toJSON()and similar methods: Any broad serialization method likely exposes more than intended. Always review what fields these methods return. -
Implement field allowlists: When transmitting data externally, explicitly construct objects with only necessary fields:
```typescript
// ❌ Dangerous
const payload = JSON.stringify(complexObject);
// ✅ Safe
const payload = JSON.stringify({
field1: complexObject.getSafeField1(),
field2: complexObject.getSafeField2()
});
```
-
Add per-operation consent: For LLM integrations, require explicit user confirmation before each external API call, with clear disclosure of what data will be sent.
-
Support local-only modes: Ensure local LLM functionality is robust and clearly distinguishable from cloud-based processing.
Detection & Standards
- CWE-200: Exposure of Sensitive Information to an Unauthorized Actor
- OWASP Top 10 2021: A01:2021 – Broken Access Control (data exposure variant)
- NIST Privacy Framework: Data Minimization and Purpose Specification
Tools for Detection
Static analysis rules can identify this pattern:
- Calls to toJSON() or similar serialization methods where the result flows to external network requests
- JSON.stringify() with complex object arguments that originate from sensitive data sources
- Missing field filtering before external API transmission
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 onlytitle,abstractNote, anddatebefore 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.