Back to skill

Security audit

zotero-pdf-upload

Security checks for vulnerabilities and agentic risk

Overview

This Zotero skill appears purpose-built for Zotero uploads, but its setup can expose a powerful API key in command history and plaintext config.

Install only if you are comfortable granting Zotero API access to this skill. Prefer setting ZOTERO_API_KEY or using a separate restricted secret file instead of the documented one-line setup, create the least-privilege Zotero key you can, avoid broad group write permissions unless needed, and review config.json permissions if it contains a key.

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/setup.py:40
Finding
Zotero API Key Exposed Through Command-Line Arguments and Plaintext Configuration<![CDATA[ ## Vulnerability Details **File Location**: `scripts/setup.py:40-61` **Additional Documentation Locations**: `SKILL.md:31-36`, `README.md:42-47` **Vulnerability Type**: Plaintext credential exposure and insecure secret storage **Risk Level**: Medium ### Vulnerable Code ```python url = sys.argv[1].strip() api_key = sys.argv[2].strip() if not url: print("Error: URL cannot be empty.") return 1 if not api_key: print("Error: API key cannot be empty.") return 1 config = TEMPLATE.copy() config["zotero"] = {**TEMPLATE["zotero"], "url": url, "apiKey": api_key} if CONFIG_PATH.exists(): print(f"⚠️ Config already exists at {CONFIG_PATH}") answer = input(" Overwrite? [y/N] ").strip().lower() if answer not in ("y", "yes"): print(" Aborted.") return 0 CONFIG_PATH.write_text(json.dumps(config, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") ``` The documented setup command directly includes the secret: ```bash python scripts/setup.py "<YOUR_LIBRARY_URL>" "<YOUR_API_KEY>" ``` ### Technical Analysis The setup workflow receives the Zotero API key through `sys.argv`. Command-line arguments can be exposed through shell history, process inspection utilities, operating-system auditing, terminal recordings, and command telemetry. The key is subsequently placed in the `apiKey` configuration field and written in plaintext to `config.json`. The code does not explicitly create or change the file to owner-only permissions. For a newly created file, effective permissions therefore depend on the process umask. For an existing file, `write_text()` generally retains its existing permissions. Although `.gitignore` excludes `config.json`, this only reduces accidental source-control commits. It does not protect the key from other local accounts, processes, backups, endpoint monitoring, or overly broad file permissions. The documentation recommends a key with library access, notes access, write access, and potentiall ...[truncated 1614 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. **Remove the API key from command-line arguments** - Accept the key through the `ZOTERO_API_KEY` environment variable, or prompt interactively with `getpass.getpass()`. - Do not include real secrets in commands that may be saved to shell history. 2. **Avoid inline secret storage by default** - Generate configuration containing only `apiKeyEnv` or `apiKeyPath`. - Keep the `apiKey` field empty unless the user explicitly chooses the less secure fallback. 3. **Enforce restrictive file permissions** - If a dedicated secret file is created, use owner-only mode `0600`. - Validate existing secret-file permissions and warn or refuse when group or world access is present. - If inline storage remains supported, explicitly protect `config.json` with mode `0600`. 4. **Reduce credential privileges** - Instruct users to grant only the library and write permissions required for their intended workflow. - Do not request notes access unless note management is actually needed. - Limit group permissions to explicitly required groups where Zotero supports that configuration. 5. **Improve documentation** - Replace the positional-key setup example with an environment-based or interactive workflow. - Clearly warn that inline configuration is plaintext and that `.gitignore` does not provide local access control. 6. **Add security tests** - Test that generated secret-bearing files have owner-only permissions. - Test that the recommended setup path does not place the API key in `sys.argv` or generated general-purpose configuration. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (9)

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The file presents all operational instructions in Chinese and does not state that the user can choose another language. Under the policy criteria, forcing a specific language without user opt-in is a natural-language policy violation unless the locale constraint is clearly documented and justified.

Intent-Code Divergence

Medium
Confidence
77% confidence
Finding
The documentation states at L061 that all write operations require explicit confirmation, suggesting a safety boundary. However, the setup instructions explicitly tell users to create an API key with write access, and this README section does not describe any enforcement mechanism for confirmation at the API credential level; this creates a contradiction between the safety claim and the documented operational setup.

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill performs privileged actions involving environment access, local file reads/writes, and network requests, but it does not declare any explicit tool scope or permission boundaries. This increases the risk of overbroad execution in agent runtimes, because users and orchestrators cannot easily see or enforce what capabilities the skill is expected to use before it handles secrets and performs write operations against Zotero.

External Transmission

Medium
Category
Data Exfiltration
Content
Never print full API keys in output.

Security note: when resolving a personal library URL (username-based, no numeric ID),
the skill calls `GET https://api.zotero.org/keys/{apiKey}` to look up the userID.
This is the standard Zotero API pattern — the key appears in the URL and may be
visible in server access logs. Use a least-privilege key and prefer env/file loading
over inline config.
Confidence
95% confidence
Finding
The skill explicitly documents a workflow that sends the Zotero API key in the request URL path (`/keys/{apiKey}`), which can expose the secret to intermediary logs, proxies, browser history equivalents, and server access logging. Even though the file warns about this, the behavior still creates a real secret-leakage risk if the key is later harvested and reused to access or modify the user's Zotero library.

Context-Inappropriate Capability

Medium
Confidence
95% confidence
Finding
The setup script takes a Zotero API key from the command line and writes it directly into config.json, creating persistent plaintext secret storage on disk. This increases the exposure surface because the key may be recovered from the file itself, backups, shell history, or accidental repository commits, and it is not necessary given the template already supports apiKeyEnv and apiKeyPath.

Missing User Warnings

Medium
Confidence
97% confidence
Finding
The script writes user-supplied credentials to disk without warning the user that the API key will be stored in plaintext in config.json. In this skill context, the key grants access to a Zotero personal or group library, so silent local persistence can lead to unauthorized library access if the file is exposed through local compromise, syncing, backups, or accidental sharing.

External Transmission

Medium
Category
Data Exfiltration
Content
The Zotero API returns the userID associated with the given key.
    This is used when a personal URL contains a username instead of an ID.
    """
    url = f"https://api.zotero.org/keys/{api_key}"
    req = urllib.request.Request(
        url,
        method="GET",
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
This code sends the Zotero API key to a remote endpoint as part of the request URL when resolving a user ID. Although the function has an internal docstring, there is no user-facing prompt, log, or warning that credentials will be used in a network call, which matches the missing-warning criterion for code handling sensitive credentials and outbound transmission.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The function reads a local PDF file and posts its bytes to a remote upload URL, transmitting user data off-system. The code contains no confirmation prompt, user-visible log/print, or nearby disclosure warning that local file contents and metadata will be uploaded, so this safety-relevant behavior lacks user warning within the code.

Static analysis

No suspicious patterns detected.