Back to skill

Security audit

Biomedical Patent Trends

Security checks for vulnerabilities and agentic risk

Overview

The skill is mostly coherent for downloading and analyzing patent wiki data, but its downloader can send the user's SciMiner API key to arbitrary URLs returned by the provider.

Review before installing. Use only with a SciMiner API key you can rotate, avoid running it against untrusted or unexpected provider results, and prefer a fixed allowlist plus header-free package downloads before use. Keep downloaded corpora and manifests private unless you have checked them for task IDs, paths, raw API responses, and private patent text.

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 (2)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/download_wiki.py:43
Finding

Authentication Token Forwarded to Untrusted Response-Provided URLs

Content
View full analysis

Vulnerability Details

File Location: scripts/download_wiki.py:43-45, scripts/download_wiki.py:65-70, scripts/download_wiki.py:93, and scripts/download_wiki.py:138-145
Vulnerability Type: Credential disclosure through unrestricted outbound requests
Risk Level: High

Vulnerable Code

python
def find_urls(value: Any) -> list[str]:
    urls: list[str] = []
    if isinstance(value, dict):
        for item in value.values():
            urls.extend(find_urls(item))
    elif isinstance(value, list):
        for item in value:
            urls.extend(find_urls(item))
    elif isinstance(value, str):
        if value.startswith(("https://", "http://")):
            urls.append(value)
        else:
            try:
                urls.extend(find_urls(json.loads(value)))
            except json.JSONDecodeError:
                pass
    return list(dict.fromkeys(urls))
python
def download(url: str, destination: Path, headers: dict[str, str]) -> None:
    with requests.get(url, headers=headers, stream=True, timeout=120) as response:
        response.raise_for_status()
        with destination.open("wb") as handle:
            for chunk in response.iter_content(chunk_size=1024 * 1024):
                if chunk:
                    handle.write(chunk)
python
headers = {"X-Auth-Token": api_key(), "Content-Type": "application/json"}
python
urls = find_urls(result.get("result", result))
if not urls:
    raise RuntimeError("Task succeeded but no HTTP(S) download URL was found. Inspect provider_result.json.")
for number, url in enumerate(urls, start=1):
    name = archive_name(url, number)
    archive = outdir / name
    download(url, archive, headers)

Technical Analysis

The downloader recursively treats every HTTP or HTTPS string in the provider result as a package URL. It does not validate the destination against an allowli ...[truncated 1702 chars]

Remediation
View remediation

Remediation Suggestions

  1. Require HTTPS for all package downloads and reject every http:// URL.
  2. Maintain an explicit allowlist of expected SciMiner and object-storage hostnames.
  3. Use separate request headers:
    • Include X-Auth-Token only for trusted SciMiner API endpoints.
    • Use a header-free request for signed storage URLs unless provider documentation explicitly requires authentication.
  4. Validate the scheme, hostname, port, and resolved final URL before downloading.
  5. Disable automatic redirects or validate every redirect target against the same allowlist.
  6. Replace recursive extraction of every URL with parsing of documented response fields.
  7. Avoid including Content-Type: application/json in download requests where it is unnecessary.
  8. Revoke and rotate any credential that may have been exposed through the current implementation.
  9. Add tests proving that unapproved hosts, HTTP URLs, and cross-host redirects are rejected and never receive the authentication header.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/download_wiki.py:54
Finding

Unbounded Remote Archive Download and ZIP Extraction

Content
View full analysis

Vulnerability Details

File Location: scripts/download_wiki.py:54-61, scripts/download_wiki.py:65-70, and scripts/download_wiki.py:145-154
Vulnerability Type: Resource exhaustion through oversized downloads or ZIP bombs
Risk Level: Medium

Vulnerable Code

python
def safe_extract(archive: Path, destination: Path) -> None:
    with zipfile.ZipFile(archive) as zf:
        root = destination.resolve()
        for member in zf.infolist():
            target = (destination / member.filename).resolve()
            if target != root and root not in target.parents:
                raise RuntimeError(f"Refusing unsafe ZIP member: {member.filename}")
        zf.extractall(destination)
python
def download(url: str, destination: Path, headers: dict[str, str]) -> None:
    with requests.get(url, headers=headers, stream=True, timeout=120) as response:
        response.raise_for_status()
        with destination.open("wb") as handle:
            for chunk in response.iter_content(chunk_size=1024 * 1024):
                if chunk:
                    handle.write(chunk)
python
for number, url in enumerate(urls, start=1):
    name = archive_name(url, number)
    archive = outdir / name
    download(url, archive, headers)
    with archive.open("rb") as handle:
        digest = hashlib.file_digest(handle, "sha256").hexdigest()
    item: dict[str, Any] = {"url": url, "path": str(archive), "sha256": digest}
    if zipfile.is_zipfile(archive):
        extracted = outdir / archive.stem
        extracted.mkdir(exist_ok=True)
        safe_extract(archive, extracted)

Technical Analysis

The path-containment check correctly blocks conventional ZIP path traversal. However, neither the download nor extraction process enforces limits on:

  • Total downloaded bytes
  • Declared Content-Length
  • Number of ZIP members
  • Total uncompressed size
  • Individual me ...[truncated 1457 chars]
Remediation
View remediation

Remediation Suggestions

  1. Define a maximum permitted archive download size and stop streaming once that limit is exceeded.
  2. Reject responses whose Content-Length exceeds the configured limit, while still enforcing the streaming limit when the header is absent or inaccurate.
  3. Before extraction, inspect ZipInfo entries and enforce:
    • A maximum member count
    • A maximum size per member
    • A maximum aggregate uncompressed size
    • A reasonable maximum compression ratio
  4. Check available disk space before downloading and again before extraction.
  5. Extract members individually while tracking actual bytes written instead of calling unrestricted extractall().
  6. Write downloads to a temporary partial file and atomically rename it only after successful validation.
  7. Delete partial downloads and partially extracted directories when validation or extraction fails.
  8. Optionally require expected archive media types and reject unsupported package formats.
  9. Add regression tests using oversized streams, excessive member counts, and high-compression ZIP fixtures.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (12)

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding

The code is narrowly focused on retrieval: it authenticates using a local SciMiner API key, invokes a remote Patent-Mol-Wiki provider task, polls for results, downloads returned archives, hashes them, and safely extracts ZIP files. This partially matches the 'download' portion of the description, but the declared description emphasizes broad search and analysis capabilities that are absent from this code chunk. Because the actual implemented behavior is only package acquisition/extraction and remote API interaction, the declared purpose materially overstates what this code does.

Content

No source excerpt is available for this finding.

Credential Access

High
Category
Privilege Escalation
Confidence
95% confidence
Finding

The skill declares a credential file containing an API key and instructs the agent to read it for outbound requests. Even though the text says not to print the key, granting the skill direct access to a reusable secret creates a real exposure path if the skill, downstream scripts, logs, or prompt-injection in downloaded content mishandle that secret.

Content

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

md
name: biomedical-patent-trends
description: Download, search, and analyze Patent-Mol-Wiki biomedical-patent packages. Use when users ask what happened in a recent patent period, need portfolio or trend statistics from wiki index.md files, ask whether a target, disease, modality, organization, or patent is present, need charts of patent distributions, or need molecular-structure analysis from selected patent folders.
credential_files:
  - ~/.config/sciminer/credentials.json
---

# Biomedical Patent Trends

Credential Access

High
Category
Privilege Escalation
Confidence
96% confidence
Finding

This step operationalizes secret use by telling the agent to read the API key from disk and send it as an HTTP header. That is dangerous because it combines local secret access with network egress, so any misrouted request, script compromise, or unintended tool use could disclose or misuse the credential.

Content

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

md
## Prepare or update the corpus

1. Read [Patent-Mol-Wiki API reference](references/patent-mol-wiki-api.md) before invoking the service. Read the API key only from `~/.config/sciminer/credentials.json`, send it as `X-Auth-Token`, and never print, persist, or place it in prompts, logs, or repository files.
2. For a period request, use `scripts/download_wiki.py --date-range recent_week --outdir <project>/data`. Use `--patent-ids` only when the user supplied patent numbers; it overrides the date range. Preserve the script's task manifest and raw service response beside the downloaded archive.
3. Locate the extracted wiki roots and their `index.md` files with `rg --files <data-root> -g index.md`. Treat each index as the authoritative aggregate view for its own wiki folder.
4. Create an analysis workspace outside the corpus (for example, `<project>/analysis/<period>/`) so downloads remain immutable and derived artifacts are separable.

Credential Access

High
Category
Privilege Escalation
Confidence
95% confidence
Finding

The credential prerequisite explicitly instructs storing a long-lived API key in a predictable filesystem location and having the skill consume it. Predictable secret locations increase the blast radius of any over-permissive file access or prompt-induced file reads, especially in a skill that also has network and file-write behavior.

Content

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

md
## SciMiner credential prerequisite

1. Obtain a SciMiner API key from `https://sciminer.tech/utility`.
2. Store it outside this repository at `~/.config/sciminer/credentials.json` as `{"api_key":"your_api_key_here"}`.
3. For every SciMiner call, read the `api_key` field from that file and send it as the `X-Auth-Token` header.
4. Never request, print, persist, or write the key in prompts, logs, output artifacts, or repository files. Remember only the credential-file path.

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/download_wiki.py (reported line 24)May include surrounding context.

python
def api_key() -> str:
    credential_file = Path.home() / ".config" / "sciminer" / "credentials.json"
    if credential_file.exists():
        try:
            value = json.loads(credential_file.read_text(encoding="utf-8")).get("api_key")

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/download_wiki.py (reported line 32)May include surrounding context.

python
def api_key() -> str:
    credential_file = Path.home() / ".config" / "sciminer" / "credentials.json"
    if credential_file.exists():
        try:
            value = json.loads(credential_file.read_text(encoding="utf-8")).get("api_key")

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding

The skill performs file reads/writes and network-backed data retrieval but does not declare any explicit tool scope or allowed-tools boundary. That increases the chance the agent can invoke broader capabilities than intended, making credentialed network access and local file operations harder to constrain or audit.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

The invocation text is broad enough to match many ordinary analytical requests, which can cause the skill to trigger in situations the user did not intend. Because this skill can download external data, read credential files, and write analysis artifacts, over-broad activation expands the chance of unnecessary credentialed access and data handling.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
78% confidence
Finding

The skill requires persisting detailed manifests including source paths, provider task IDs, request parameters, timestamps, and commands. While useful for reproducibility, this creates session persistence of potentially sensitive operational metadata, which can expose internal paths, query history, provider identifiers, or private corpus provenance if artifacts are later shared or indexed.

Content

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

md
## Reproducibility and safety

- Write source package path/hash, provider task ID, request parameters, retrieval timestamp, script command, and tool versions to the analysis manifest.
- Never publish credentials, unredacted private patent text, or an API response containing a credential.
- Do not present application counts as unique inventions without family-level identifiers and a documented deduplication rule.
- Treat title/abstract/description matches as retrieval evidence, not proof that a claim covers the target, disease, or structure. Read claims for scope conclusions.

External Transmission

Medium
Category
Data Exfiltration
Confidence
80% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/download_wiki.py (reported line 112)May include surrounding context.

python
"tool_name": "download_wiki_data_download_wiki_data_post",
            "parameters": parameters,
        }
        submitted = requests.post(INVOKE_URL, json=payload, headers=headers, timeout=30)
        submitted.raise_for_status()
        task_id = submitted.json().get("task_id")
        if not task_id:

Tainted flow: 'task_id' from requests.post (line 114, network input) → requests.get (network output)

Medium
Category
Data Flow
Confidence
65% confidence
Finding

Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Content

Scanner excerpt · scripts/download_wiki.py (reported line 123)May include surrounding context.

python
result: dict[str, Any] = {}
    for attempt in range(1, args.max_polls + 1):
        response = requests.get(RESULT_URL, params={"task_id": task_id}, headers=headers, timeout=20)
        response.raise_for_status()
        result = response.json()
        status = result.get("status")

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
87% confidence
Finding

The docstring says the package is downloaded 'without persisting credentials,' which implies credential handling is side-effect-free or non-stored. In reality, the script depends on a persisted API key at ~/.config/sciminer/credentials.json and uses it for authenticated network calls, so the documentation overstates the credential-safety property.

Content

No source excerpt is available for this finding.