Back to skill

Security audit

zotero-write

Security checks for vulnerabilities and agentic risk

Overview

This skill does what it claims, but it directly changes a local Zotero database and its backup safeguard can be bypassed by mismatched paths.

Review this before installing if your Zotero library is important or shared. Use it only for explicit Zotero write requests, close Zotero first, keep an independent backup of the actual zotero.sqlite file, and avoid custom --db/--backup combinations unless you verify they point to the same database.

Vulnerability Patterns
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (1)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/write_items.py:24
Finding

Backup Validation Does Not Protect the Database Being Modified

Content
View full analysis

Vulnerability Details

File Location: scripts/write_items.py, lines 24–26 and 312–322
Vulnerability Type: Backup safeguard bypass due to unbound source and target paths
Risk Level: Medium

Vulnerable Code

python
parser.add_argument("--db", default=DEFAULT_DB, help="Path to zotero.sqlite")
parser.add_argument("--backup", metavar="PATH",
                    help="Backup the database to BACKUP_DIR before writing")
python
# All write operations require backup
has_write = any([args.add_tag, args.add_note, args.set_field, args.new_item])
if has_write and not args.backup:
    print("ERROR: --backup DB_PATH is required before making any write changes.")
    print("Example: --backup \"E:\\Refer.Hub\\zotero.sqlite\"")
    sys.exit(1)

# Backup
if args.backup:
    backup_path = backup_db(args.backup)
    print(f"[WRITE MODE] Database: {args.db}")

conn = sqlite3.connect(args.db, timeout=60)

The backup function copies the independently supplied path:

python
def backup_db(db_path):
    os.makedirs(BACKUP_DIR, exist_ok=True)
    ts = datetime.now().strftime("%Y%m%d_%H%M%S")
    backup_path = os.path.join(BACKUP_DIR, f"zotero_{ts}.sqlite")
    shutil.copy2(db_path, backup_path)
    print(f"[BACKUP] Created: {backup_path}")
    return backup_path

Technical Analysis

The script requires the presence of --backup before a write operation, but it does not bind that argument to the database specified by --db. The file copied by backup_db(args.backup) can therefore differ from the file subsequently opened and modified by sqlite3.connect(args.db).

This is a fail-open validation flaw. The safeguard verifies only that a backup argument was supplied, not that a recoverable copy of the write target was created. Path aliases, symbolic links, relative paths, or completely unrelated readable files can satisfy the check.

The behavior contradicts the documented guarantee that write operations always back up the affected Zotero databa ...[truncated 1689 chars]

Remediation
View remediation

Remediation Suggestions

Make the database selected by --db the sole authoritative backup source. The safest interface is to change --backup into a Boolean flag:

python
parser.add_argument(
    "--backup",
    action="store_true",
    help="Back up the database selected by --db before writing"
)

Then back up args.db directly:

python
if has_write and not args.backup:
    parser.error("--backup is required for write operations")

if has_write:
    backup_path = backup_db(args.db)

Additional hardening should include:

  1. Resolve the source path with os.path.realpath() or pathlib.Path.resolve() before both backup and connection operations.
  2. If backward compatibility requires --backup PATH, resolve both args.backup and args.db, compare them, and reject any mismatch.
  3. Verify that the source is a regular file and that the completed backup exists with a nonzero size before allowing writes.
  4. Prefer SQLite's Connection.backup() API over shutil.copy2() so the backup remains transactionally consistent if the database is in WAL mode or another process has it open.
  5. Generate collision-resistant backup names by including microseconds or using an exclusive temporary filename; the existing one-second timestamp can overwrite a backup created during the same second.
  6. Abort before opening the write connection if backup creation or verification fails.
  7. Add automated tests confirming that mismatched paths, aliases, symbolic links, and backup failures prevent all write operations.
Vulnerability Patterns
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
Findings (3)

Self-Modification

High
Category
Rogue Agent
Confidence
85% confidence
Finding

Skill modifies its own code, configuration, or behavior at runtime. Self-modification enables an agent to escalate privileges, disable safety constraints, or install persistent backdoors.

Content

Scanner excerpt · SKILL.md (reported line 13)May include surrounding context.

md
primaryEnv: null
---

# Zotero Write Skill

Write, tag, annotate, and edit a local Zotero SQLite database.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding

The skill performs local database writes but does not declare any explicit tool scope or allowed-tools restrictions. That creates an authorization ambiguity where an agent may invoke file-writing capabilities more broadly than intended, increasing the chance of unintended modification of local data.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The activation text includes broad phrases like 'edit Zotero' and 'any request to modify, annotate, or write to the Zotero database,' which can cause the skill to trigger on ambiguous user requests. Because this skill performs destructive writes, accidental invocation could lead to unauthorized metadata changes, notes, tags, or item creation in the user's library.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.