Back to skill

Security audit

Zotero

Security checks across malware telemetry and agentic risk

Overview

This is a coherent Zotero management skill, but it can read and change a Zotero library when given an API key.

Install only if you want an agent to manage your Zotero library. Use a least-privilege Zotero API key, verify whether it targets your personal or group library, scope bulk operations with --limit or --collection, run dry-run modes first, and be especially careful with destructive or mutating flags such as --yes, --permanent, --apply, --upload, --download-dir, --force, and --output.

SkillSpector

By NVIDIA
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (9)

Tainted flow: 'req' from os.environ.get (line 1369, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
url = f"https://api.unpaywall.org/v2/{urllib.parse.quote(doi, safe='')}?email={CROSSREF_EMAIL}"
    req = urllib.request.Request(url, headers={"Accept": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=15) as resp:
            data = json.loads(resp.read().decode("utf-8"))
        oa = data.get("best_oa_location") or {}
        pdf_url = oa.get("url_for_pdf")
Confidence
88% confidence
Finding
with urllib.request.urlopen(req, timeout=15) as resp:

Tainted flow: 'req' from os.environ.get (line 1369, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Content
"Accept": "application/pdf,*/*",
    })
    try:
        with urllib.request.urlopen(req, timeout=60) as resp:
            with open(dest_path, "wb") as f:
                shutil.copyfileobj(resp, f)
        # Verify it's actually a PDF (check magic bytes)
Confidence
79% confidence
Finding
with urllib.request.urlopen(req, timeout=60) as resp:

Lp3

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding
The skill clearly requires environment secrets and performs network, file read, and file write operations, yet it does not declare explicit permissions beyond informal metadata. That mismatch weakens review and runtime governance because users or orchestrators may invoke a capability-bearing skill without clear consent boundaries, especially since it can write to a Zotero library, download PDFs locally, and use API credentials.

Vague Triggers

Medium
Confidence
82% confidence
Finding
The activation text is broad enough to trigger on generic academic-assistance requests, not just explicit Zotero management. In a capability-bearing skill, overbroad routing increases the chance the agent invokes a tool with network, filesystem, and library-write powers when a safer non-tool response would suffice, raising the risk of unintended external access or state changes.

Missing User Warnings

Medium
Confidence
86% confidence
Finding
The delete command supports --yes, allowing immediate trashing or permanent deletion of one or more items without an execution-time safety interlock. In an agent context, this is more dangerous than a normal local CLI because an automated workflow could perform destructive library changes without an additional human confirmation step.

External Transmission

Medium
Category
Data Exfiltration
Content
}
    if first_author:
        params["query.author"] = first_author
    url = "https://api.crossref.org/works?" + urllib.parse.urlencode(params)
    req = urllib.request.Request(url, headers={"Accept": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=20) as resp:
Confidence
80% confidence
Finding
https://api.crossref.org/

External Transmission

Medium
Category
Data Exfiltration
Content
def _try_unpaywall(doi):
    """Try Unpaywall for an OA PDF URL. Returns (pdf_url, source_url) or None."""
    url = f"https://api.unpaywall.org/v2/{urllib.parse.quote(doi, safe='')}?email={CROSSREF_EMAIL}"
    req = urllib.request.Request(url, headers={"Accept": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=15) as resp:
Confidence
84% confidence
Finding
https://api.unpaywall.org/

External Transmission

Medium
Category
Data Exfiltration
Content
def _try_semantic_scholar(doi):
    """Try Semantic Scholar for an OA PDF URL. Returns (pdf_url, source_url) or None."""
    url = f"https://api.semanticscholar.org/graph/v1/paper/DOI:{urllib.parse.quote(doi, safe='')}?fields=openAccessPdf"
    req = urllib.request.Request(url, headers={"Accept": "application/json"})
    try:
        with urllib.request.urlopen(req, timeout=15) as resp:
Confidence
80% confidence
Finding
https://api.semanticscholar.org/

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
# delete
    p = subparsers.add_parser("delete", help="Move items to trash (default) or permanently delete")
    p.add_argument("keys", nargs="+", help="Item key(s) to delete")
    p.add_argument("--yes", action="store_true", help="Skip confirmation")
    p.add_argument("--permanent", action="store_true", help="Permanently delete (default is recoverable trash)")
    p.add_argument("--trash", action="store_true", help="Move to trash (default, kept for backwards compat)")
Confidence
88% confidence
Finding
Skip confirmation

VirusTotal

63/63 vendors flagged this skill as clean.

View on VirusTotal

Static analysis

No suspicious patterns detected.