Back to skill

Security audit

zotero-myscholar

Security checks for vulnerabilities and agentic risk

Overview

This Zotero helper has a coherent goal, but the shipped script contains a credential-shaped secret, does not match its credential documentation, and can automatically download and upload poorly validated URLs.

Review this carefully before installing. Use a narrowly scoped Zotero API key, rotate any key matching the exposed credential-shaped string if it is real, and avoid running the script on untrusted URLs until the credential lookup, arXiv host validation, download limits, and attachment opt-in behavior are fixed.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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
Findings (3)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/save_paper.py:24
Finding
Exposed Zotero API Credential in Source Code<![CDATA[ ## Vulnerability Details **File Location**: `scripts/save_paper.py`, line 24 **Vulnerability Type**: Hardcoded secret and incorrect environment-variable lookup **Risk Level**: High ### Vulnerable Code ```python zotero_creds = os.environ.get('19883603:YtIe0tqZtA12wBvFDTB8EIRR') # ID:KEY if zotero_creds and ':' in zotero_creds: try: parts = zotero_creds.strip().split(':') if len(parts) == 2: library_id = parts[0].strip() api_key = parts[1].strip() except: pass ``` ### Technical Analysis The argument passed to `os.environ.get()` is a credential-shaped value containing a Zotero user ID and API key rather than the documented environment-variable name, `ZOTERO_CREDENTIALS`. Consequently: 1. A likely Zotero API credential is exposed to anyone who can read the source code, package, repository history, build logs, or distributed artifact. 2. The application does not read the documented `ZOTERO_CREDENTIALS` variable. 3. Authentication normally fails unless the process environment contains a variable whose name is the entire embedded credential string. The exposed key's actual permissions cannot be established from the audited files. However, the application expects credentials capable of searching a Zotero library, creating items and notes, and uploading attachments. ### Attack Path 1. An attacker obtains a copy of the source package or accesses its repository history. 2. The attacker extracts the embedded Zotero user ID and API key from line 24. 3. The attacker submits the credential to the Zotero API. 4. If the key remains valid, the attacker reads or modifies resources allowed by its configured Zotero permissions. 5. For a write-enabled key, the attacker may create unwanted records or attachments and corrupt the associated library's integrity. ### Impact Assessment A valid exposed key could permit unauthorized access to the associated Zotero account or library within the key's configured scop ...[truncated 344 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Revoke the exposed Zotero API key immediately and issue a replacement with the minimum necessary permissions. 2. Remove the credential from the current source and repository history. 3. Correct the lookup: ```python zotero_creds = os.environ.get("ZOTERO_CREDENTIALS") ``` 4. Validate the credential using a bounded split and reject empty components: ```python zotero_creds = os.environ.get("ZOTERO_CREDENTIALS", "") try: library_id, api_key = (part.strip() for part in zotero_creds.split(":", 1)) except ValueError: library_id = api_key = "" if not library_id or not api_key: raise SystemExit("ZOTERO_CREDENTIALS must use the userID:apiKey format") ``` 5. Store production secrets in a secret manager or protected runtime environment rather than source files. 6. Enable automated secret scanning in source-control and CI pipelines. 7. Review Zotero access logs and library changes for unauthorized activity involving the exposed key. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/save_paper.py:91
Finding
Server-Side Request Forgery Through Insufficient arXiv URL Validation<![CDATA[ ## Vulnerability Details **File Location**: `scripts/save_paper.py`, lines 91-121 **Vulnerability Type**: Unrestricted network request and unsafe URL allowlist check **Risk Level**: High ### Vulnerable Code ```python # Download and attach PDF if 'arxiv.org' in args.url: try: import urllib.request import tempfile # Convert the abstract link to a PDF link pdf_url = args.url.replace('/abs/', '/pdf/') if not pdf_url.endswith('.pdf'): pdf_url += '.pdf' print(f"Downloading PDF...") # Set User-Agent for download support opener = urllib.request.build_opener() opener.addheaders = [('User-agent', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Chrome/120.0.0.0')] urllib.request.install_opener(opener) # Create a safe filename safe_title = "".join(c for c in args.title if c.isalnum() or c in (" ", "-", "_")).strip() safe_title = safe_title[:50] safe_filename = f"{safe_title}.pdf" # Use a temporary directory with a specified filename with tempfile.TemporaryDirectory() as td: pdf_path = os.path.join(td, safe_filename) urllib.request.urlretrieve(pdf_url, pdf_path) print(f"Uploading PDF attachment ({safe_filename})...") zot.attachment_simple([pdf_path], item_key) print("PDF attached.") except Exception as e: print(f"Failed to attach PDF: {e}", file=sys.stderr) ``` ### Technical Analysis The program attempts to restrict automatic downloads to arXiv by testing whether the untrusted URL contains the substring `arxiv.org`. A substring test does not establish the parsed hostname, scheme, destination address, or redirect target. Examples of URLs that can satisfy the test without targeting the legitimate arXiv service include: ```text https://arxiv.org.attacker.example/file http://127.0.0.1/arxiv.org https://attacker.example/arxiv.org ``` The code ...[truncated 1716 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse the URL using `urllib.parse.urlsplit()` instead of applying a substring check. 2. Require the `https` scheme. 3. Permit only the exact hostname `arxiv.org` and explicitly approved arXiv subdomains. 4. Reject embedded credentials, unexpected ports, malformed hostnames, and non-HTTP schemes. 5. Resolve the hostname and reject loopback, private, link-local, multicast, reserved, and unspecified IP addresses for every resolved address. 6. Disable automatic redirects or validate every redirect destination against the same allowlist. 7. Construct the PDF URL from a validated arXiv identifier rather than modifying an arbitrary URL supplied by the caller. 8. Stream the response with strict connection/read timeouts and a maximum byte limit. 9. Validate the HTTP status, `Content-Type`, and PDF signature before uploading the file. 10. Run the skill with restricted outbound network access so it cannot contact internal or link-local services. ]]>

T08 · Insecure Dependencies

Warning
Location
scripts/save_paper.py:2
Finding
Unpinned Runtime Dependency Allows Unreviewed Package Updates<![CDATA[ ## Vulnerability Details **File Location**: `scripts/save_paper.py`, lines 2-5 **Vulnerability Type**: Unpinned third-party dependency **Risk Level**: Medium ### Vulnerable Code ```python # /// script # requires-python = ">=3.10" # dependencies = ["pyzotero>=1.6.0"] # /// ``` ### Technical Analysis The inline dependency declaration accepts any `pyzotero` version greater than or equal to `1.6.0`. No lockfile, exact version, or integrity hash is present in the audited project. When the script is executed through `uv run`, dependency resolution may select a newer package release that was not reviewed when this skill was audited. This creates a non-reproducible execution environment and increases exposure to compromised upstream releases, malicious maintainer activity, or incompatible future changes. No evidence was found that the currently named `pyzotero` package is malicious. The finding concerns unsafe dependency resolution rather than a confirmed malicious package. ### Attack Path 1. A compromised maintainer account or upstream package infrastructure publishes a malicious `pyzotero` release satisfying `>=1.6.0`. 2. A user runs the skill in an environment that has not already locked or cached a known-good release. 3. `uv` resolves and installs the malicious compatible release. 4. Python imports the package through `from pyzotero import zotero`. 5. Package initialization or invoked package functions execute with the privileges of the user running the skill. 6. The malicious dependency may access the Zotero credential, local files available to that user, and the process's network connections. ### Impact Assessment A compromised dependency could execute arbitrary Python code with the operating-system privileges of the skill process. It could potentially read runtime secrets, modify user-accessible files, alter Zotero operations, or send data over the network. The practical likelihood depends on an upstream compromise or unsafe future release. T ...[truncated 77 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pin `pyzotero` to an exact version that has been reviewed and tested: ```python # dependencies = ["pyzotero==<reviewed-version>"] ``` 2. Generate and commit a `uv` lockfile where the execution model supports it. 3. Require package hashes or other integrity verification in the dependency installation process. 4. Use a controlled package index or internal mirror for production execution. 5. Automate vulnerability and provenance scanning for dependencies. 6. Update the pinned version only through a reviewed dependency-update process. 7. Run the skill in a least-privileged environment with restricted filesystem and network access to reduce the impact of a supply-chain compromise. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (9)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding
The documented behavior materially understates what the code actually does: it searches Zotero, creates notes, downloads remote PDFs from arXiv, and uploads attachments. Hidden or underdocumented network/file operations are dangerous because they can cause unintended data transfer, consume resources, and exceed the trust boundary the user believed they were granting.

Intent-Code Divergence

High
Confidence
99% confidence
Finding
The script claims to use the ZOTERO_CREDENTIALS environment variable but actually calls os.environ.get() with a hardcoded string that looks like a real Zotero userID:API-key pair. This can embed a secret identifier in source code, break expected credential handling, and may cause accidental credential disclosure or unauthorized use if the hardcoded value corresponds to a real account.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill uses sensitive capabilities—environment variable access for Zotero credentials and outbound network access to Zotero/arXiv—but the manifest does not declare a tool/permission scope. This weakens user consent and review because operators cannot easily see that the skill will access secrets and perform external network actions.

Description-Behavior Mismatch

Medium
Confidence
92% confidence
Finding
The skill presents itself as a Zotero save helper, but the code also fetches content from arXiv and uploads it as an attachment. This is a scope expansion beyond the stated purpose and can surprise users with extra external requests and content ingestion they did not explicitly authorize.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The manifest description is written as an instruction in Chinese and does not provide any language choice or note that the skill is region- or locale-specific. Under the policy, forcing a specific language without user opt-in is a natural-language policy violation.

Intent-Code Divergence

Medium
Confidence
94% confidence
Finding
The documentation suggests only storing PDF links, but the implementation downloads the full PDF and re-uploads it to Zotero for arXiv URLs. That distinction matters because downloading and storing third-party files changes the privacy, bandwidth, storage, and content-handling risk profile.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
This code reads a credential-like value from the environment and uses it to authenticate to Zotero, then sends bibliographic data to a remote service. While some progress messages exist later, there is no clear warning at the point of credential access or before transmitting user-supplied paper metadata over the network.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The script automatically downloads a PDF from a URL derived from user input and uploads it to Zotero without explicit confirmation. In an agent-skill context, this creates unreviewed outbound and inbound network activity, which can be abused to fetch unexpected content, consume resources, or attach unwanted files to a user's library.

Natural-Language Policy Violations

Low
Confidence
97% confidence
Finding
The script's CLI description, argument help, and runtime messages are all presented in Chinese, which imposes a specific language on users without opt-in or documented locale scope. This matches the policy category for language or locale constraints lacking user choice.

Static analysis

No suspicious patterns detected.