Back to skill

Security audit

Sopaper Evidence

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its research-assistance purpose, but its network fetching and automatic evidence-upgrade logic create review-worthy risks.

Install only if you are comfortable with a research helper that sends search terms to external services and fetches web pages. Avoid using it with confidential topics or untrusted URL lists until redirect-safe fetching, response-size limits, and manual approval for verified evidence labels are added.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/fetch_external_sources.py:162
Finding

Redirect-Based Server-Side Request Forgery in External Source Fetcher

Content
View full analysis

Vulnerability Details

File Location: scripts/fetch_external_sources.py:162-179; related validation in scripts/url_safety.py:19-59
Vulnerability Type: Server-Side Request Forgery through automatically followed redirects
Risk Level: High

Vulnerable Code

python
def fetch_note(url: str, *, timeout: int) -> dict[str, str]:
    assert_public_http_url(url)
    request = Request(
        url,
        headers={
            "User-Agent": "SopaperEvidenceBot/0.6 (+https://github.com/sheepxux/SoPaper-Evidence)"
        },
    )
    try:
        with urlopen(request, timeout=timeout) as response:
            content_type = response.headers.get("Content-Type", "")
            raw = response.read().decode("utf-8", errors="replace")
    except HTTPError as exc:
        if exc.code in {301, 302, 303, 307, 308} and exc.headers.get("Location"):
            redirected = urljoin(url, exc.headers["Location"])
            assert_public_http_url(redirected)
            return fetch_note(redirected, timeout=timeout)
        raise

The URL validator itself checks the supplied hostname and its currently resolved addresses:

python
def assert_public_http_url(url: str) -> None:
    parsed = urlparse(url)
    if parsed.scheme not in {"http", "https"}:
        raise ValueError("only http and https URLs are allowed")
    if not parsed.hostname:
        raise ValueError("URL must include a hostname")
    if parsed.username or parsed.password:
        raise ValueError("URLs with embedded credentials are not allowed")

    host = parsed.hostname.strip().lower().rstrip(".")
    if host in BLOCKED_HOSTS or host.endswith(".localhost") or host.endswith(".local"):
        raise ValueError(f"local host is not allowed: {host}")

    try:
        if not is_public_ip(host):
            raise ValueError(f"non-public IP address is not allowed: {host}")
        return
    except ValueError a
...[truncated 3123 chars]
Remediation
View remediation

Remediation Suggestions

  • Disable the default automatic redirect handler.
  • Process redirects manually and call assert_public_http_url on every resolved redirect target before connecting.
  • Enforce a small redirect limit, such as three redirects, and reject redirect loops.
  • Reject HTTPS-to-HTTP downgrade redirects.
  • Address DNS rebinding by connecting to a validated and pinned public IP while preserving the original hostname for TLS certificate and HTTP host validation, preferably through a mature SSRF-resistant HTTP client.
  • Revalidate the peer address after connection where the networking library permits it.
  • Apply outbound firewall rules that deny loopback, private, link-local, multicast, and reserved address ranges.
  • Add regression tests covering direct private URLs, public-to-private redirects, relative redirects, redirect chains, mixed DNS answers, IPv4-mapped IPv6 addresses, and DNS rebinding scenarios.

T09 · Insecure Skill Coding Practices

Error
Location
scripts/verify_source_notes.py:45
Finding

Attacker-Controlled Web Content Can Be Automatically Promoted to Verified Evidence

Content
View full analysis

Vulnerability Details

File Location: scripts/verify_source_notes.py:45-98; contributing logic in scripts/fetch_external_sources.py:285-298, 369-424 and scripts/build_evidence_ledger.py:487-498, 550-558
Vulnerability Type: Evidence poisoning and unsafe trust-boundary elevation
Risk Level: High

Vulnerable Code

The fetcher categorizes nearly any fetched page with a hostname as an official document:

python
def guess_external_source_type(locator: str, content_type: str) -> str:
    parsed = urlparse(locator)
    host = parsed.netloc.lower()
    path = parsed.path.lower()

    if any(token in host for token in ["sourcegraph.com", "theaireport.net"]) or "blog" in path:
        return "blog"
    if "arxiv.org" in host or "doi.org" in host:
        return "paper"
    if "github.com" in host or "gitlab.com" in host:
        return "repo"
    if any(token in host for token in ["readthedocs.io", "docs.", "documentation"]):
        return "official_doc"
    if any(token in host for token in ["paperswithcode.com", "huggingface.co"]):
        return "benchmark"
    if any(token in path for token in ["benchmark", "leaderboard", "eval"]):
        return "benchmark"
    if any(token in path for token in ["/dataset", "/datasets"]):
        return "dataset"
    if "html" in content_type.lower() or host:
        return "official_doc"
    return "other"

Remote text is turned into semantic facts through keyword matching:

python
if source_type in {"paper", "benchmark"} and any(
    token in lowered
    for token in [
        "benchmark",
        "dataset",
        "long-horizon",
        "robot manipulation",
        "tabletop manipulation",
        "retrieval",
        "citation",
        "grounded generation",
        "code understanding",
        "code generation",
        "translation",
    ]
):
    facts.append(f"Candidate benchmark/task fact: {trim
...[truncated 5904 chars]
Remediation
View remediation

Remediation Suggestions

  • Remove automatic conversion from fetched-primary-review-required to any verified status.
  • Require an explicit human approval field or a separate reviewer-generated attestation before assigning verified_fact.
  • Rename machine-generated statuses to accurately limited terms such as fetched_unreviewed and metadata_extracted_unverified.
  • Do not treat metadata presence, source type, or keyword matches as factual verification.
  • Replace the or host fallback with other or unverified_web_page.
  • Use narrowly defined trusted-host and trusted-identifier rules only as source-quality hints, not proof of factual accuracy.
  • Validate DOI, arXiv, OpenReview, repository-owner, dataset-owner, and publication metadata where applicable.
  • Require independent corroboration for important external claims.
  • Preserve the exact source URL, quoted excerpt, extraction location, and content hash for reviewer inspection.
  • Keep all remote text explicitly marked as untrusted data when supplied to an AI Agent.
  • Detect and quarantine instruction-like content rather than incorporating it into reviewed summaries.
  • Ensure the ledger accepts verified_fact only from a separate, auditable manual-review artifact.

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/fetch_external_sources.py:162
Finding

Unbounded External Response Read Allows Resource Exhaustion

Content
View full analysis

Vulnerability Details

File Location: scripts/fetch_external_sources.py:162-175
Vulnerability Type: Unbounded network response consumption
Risk Level: Medium

Vulnerable Code

python
def fetch_note(url: str, *, timeout: int) -> dict[str, str]:
    assert_public_http_url(url)
    request = Request(
        url,
        headers={
            "User-Agent": "SopaperEvidenceBot/0.6 (+https://github.com/sheepxux/SoPaper-Evidence)"
        },
    )
    try:
        with urlopen(request, timeout=timeout) as response:
            content_type = response.headers.get("Content-Type", "")
            raw = response.read().decode("utf-8", errors="replace")

Technical Analysis

Calling response.read() without a size argument loads the complete response body into memory. No maximum Content-Length, streamed byte budget, accepted media-type policy, or decompressed-size limit is enforced.

The socket timeout is not an adequate substitute for a body-size or total-transfer limit. A server can continuously deliver data within each timeout interval, or return a very large response quickly enough to avoid timeout. The resulting text is then decoded and parsed, causing further memory and CPU consumption.

Because the external source URL can originate from user-controlled source files, an attacker can direct the fetcher to a server designed to return oversized content.

Attack Path

  1. An attacker supplies or causes inclusion of a URL under their control.
  2. The URL passes the public-address safety check.
  3. The server responds with a very large body, a long-running stream, or content whose decoded size is much larger than expected.
  4. response.read() continues until end-of-file while accumulating the entire body in memory.
  5. UTF-8 decoding and HTML parsing create additional copies and processing overhead.
  6. The pipeline consumes excessive memory or CPU and may terminate, become unrespons ...[truncated 522 chars]
Remediation
View remediation

Remediation Suggestions

  • Set a strict maximum response size appropriate for source metadata, such as 2–10 MB.
  • Reject responses whose declared Content-Length exceeds the configured limit.
  • Read the body incrementally in bounded chunks and abort as soon as the byte budget is exceeded.
  • Enforce both per-operation timeouts and a total transfer deadline.
  • Allow only expected content types, principally HTML and bounded plain text.
  • Reject binary formats unless a dedicated, size-limited parser is used.
  • Account for decompressed size if transparent content encoding is enabled.
  • Limit the amount of text retained for HTML parsing and the number and length of extracted paragraphs.
  • Add tests using oversized fixed responses, chunked streams, slow responses, misleading Content-Length headers, and compressed expansion payloads.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (18)

Tp4

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding

The declared description emphasizes an evidence-first workflow centered on discovering real evidence, verifying sources, and grounding citations. The supplied code does none of that. It only parses a topic, classifies it via hardcoded keyword heuristics, and emits a structured markdown list of tentative claims, required evidence categories, risks, and scope limits. There is no network access, corpus access, file ingestion of research artifacts, source validation, citation handling, or evidence organization. While the generated text references evidence review and cautious claims, that is only advisory/template content, not actual evidence-first research behavior. Therefore the code's actual primary purpose materially differs from the declared purpose.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding

The declared description suggests a general research-assistance workflow focused on searching for evidence, verifying sources, and grounding citations across papers, datasets, benchmarks, and artifacts. The supplied code does none of that discovery or source-verification work. It only reads two local markdown inputs, extracts claims and evidence entries, and emits a fairness review draft specifically for comparative claims. Its checks are heuristic and limited to baseline breadth, metric grounding, scope overlap, and external fairness anchors. This is a materially different and narrower primary purpose than the declared description, so the description does not accurately represent the code's actual behavior.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding

The declared description suggests a broader research-assistance capability centered on searching for evidence, verifying sources, and grounding citations in real external materials. The supplied code does none of that. It only reads two local markdown files, extracts claims and ledger entries, and applies simple keyword/classification heuristics to produce a draft gap triage table. There is no web search, source retrieval, citation checking, external dataset/paper handling, or artifact discovery. While the script is adjacent to evidence management, its actual function is narrower and materially different: auditing existing claims/evidence drafts for likely experimental or documentation gaps. That constitutes a description-behavior mismatch.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The supplied code is a focused security utility for validating that a URL points to a public HTTP(S) endpoint. It parses URLs, rejects non-http schemes, disallows embedded credentials, blocks localhost/local domains, checks direct IPs for public routability, and resolves hostnames to ensure they do not map to private or internal addresses. This behavior does not implement searching, verifying sources, grounding citations, or organizing research artifacts as described. Because the actual code’s primary function is URL/network safety enforcement rather than evidence-first research workflow behavior, this is a material description-behavior mismatch.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding

The code does not implement a research workflow for finding, verifying, or organizing evidence. It only validates that a provided local file contains certain required headings or minimally structured result fields. For result artifacts, it parses CSV/TSV/JSON and checks for heuristic header/key names like metric, benchmark, task, or results. This is materially narrower and different from the declared purpose. While such validation could support an evidence workflow, the actual code chunk’s primary purpose is input schema validation, not evidence discovery, source verification, or citation grounding.

Content

No source excerpt is available for this finding.

Env Variable Harvesting

High
Category
Data Exfiltration
Confidence
80% confidence
Finding

Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Content

Scanner excerpt · scripts/validate_input_bundle.py (reported line 76)May include surrounding context.

python
keys = {str(key).lower() for key in payload.keys()}
            if "results" in keys and isinstance(payload.get("results"), list) and payload["results"]:
                return True
            return any(token in keys for token in ["metric", "benchmark", "baseline", "task", "results"])
        if isinstance(payload, list) and payload:
            first = payload[0]
            if isinstance(first, dict):

Env Variable Harvesting

High
Category
Data Exfiltration
Confidence
80% confidence
Finding

Code enumerates, copies, or searches environment variables for secrets. Bulk environment access can collect credentials unrelated to the skill's stated purpose.

Content

Scanner excerpt · scripts/validate_input_bundle.py (reported line 81)May include surrounding context.

python
keys = {str(key).lower() for key in payload.keys()}
            if "results" in keys and isinstance(payload.get("results"), list) and payload["results"]:
                return True
            return any(token in keys for token in ["metric", "benchmark", "baseline", "task", "results"])
        if isinstance(payload, list) and payload:
            first = payload[0]
            if isinstance(first, dict):

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding

The skill advertises and references capabilities that imply file access, shell execution, and network use, but it does not declare any explicit tool scope or permissions boundary. That creates an authorization ambiguity: a caller or runtime may grant broader capabilities than necessary, increasing the blast radius if the skill or any referenced helper script is misused, compromised, or invoked on untrusted inputs.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/run_evidence_pipeline.py (reported line 143)May include surrounding context.

python
def run_python(script: Path, arguments: list[str]) -> None:
    subprocess.run(
        [sys.executable, str(script), *arguments],
        check=True,
    )

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/run_evidence_pipeline.py (reported line 151)May include surrounding context.

python
def run_fetch_external(root: Path, sources: list[Path], output_dir: Path) -> list[Path]:
    script = root / "scripts" / "fetch_external_sources.py"
    result = subprocess.run(
        [sys.executable, str(script), *[str(path) for path in sources], "--output-dir", str(output_dir)],
        capture_output=True,
        text=True,

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/run_evidence_pipeline.py (reported line 163)May include surrounding context.

python
def run_verify_fetched(root: Path, notes: list[Path], output_dir: Path) -> list[Path]:
    script = root / "scripts" / "verify_source_notes.py"
    subprocess.run(
        [sys.executable, str(script), *[str(path) for path in notes], "--output-dir", str(output_dir)],
        check=True,
    )

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/run_evidence_pipeline.py (reported line 175)May include surrounding context.

python
return
    validator = root / "scripts" / "validate_input_bundle.py"
    for artifact in artifacts:
        subprocess.run(
            [sys.executable, str(validator), "result-artifact", str(artifact)],
            check=True,
            capture_output=True,

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/run_topic_evidence_pipeline.py (reported line 93)May include surrounding context.

python
def run_python(script: Path, arguments: list[str]) -> None:
    subprocess.run([sys.executable, str(script), *arguments], check=True)


if __name__ == "__main__":

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The script constructs search queries from user-provided topics and plan-derived content, then sends them to external services. Even though this is expected functionality for an evidence-gathering workflow, it still creates a real data-exposure risk because potentially sensitive research topics, internal project names, or proprietary plan text may be transmitted off-box without explicit consent or disclosure.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
91% confidence
Finding

This code sends the assembled query string to the OpenAlex API, which is an external third party. If the topic or plan contains confidential information, internal codenames, or sensitive investigative context, that information is disclosed to an outside service and may be logged or retained.

Content

Scanner excerpt · scripts/search_external_sources.py (reported line 259)May include surrounding context.

python
def search_openalex(query: str) -> list[dict[str, str]]:
    url = "https://api.openalex.org/works?" + urlencode(
        {
            "search": query,
            "per-page": 8,

External Transmission

Medium
Category
Data Exfiltration
Confidence
91% confidence
Finding

This code transmits user-controlled search queries to the GitHub Search API. In a research assistant context, that can leak sensitive internal topics, customer names, incident subjects, or unreleased product terms to an external platform, even though the request itself uses a legitimate API.

Content

Scanner excerpt · scripts/search_external_sources.py (reported line 320)May include surrounding context.

python
def search_github_repositories(query: str) -> list[dict[str, str]]:
    url = "https://api.github.com/search/repositories?" + urlencode(
        {
            "q": query,
            "sort": "stars",

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
85% confidence
Finding

The script performs outbound HTTP requests to every discovered URL and writes generated markdown files to a user-specified directory without any confirmation, dry-run mode, or explicit warning at execution time. In an evidence-gathering skill, these side effects are expected, but they can still surprise users, leak network metadata to third-party hosts, or create files from untrusted remote content in automated workflows.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
86% confidence
Finding

This code writes generated markdown to a user-specified path, creating parent directories if needed, but provides no confirmation prompt, notice, or user-facing message when the write occurs. For code-file SQP-2 checks, file writes that affect the filesystem should include some visible disclosure unless clearly communicated elsewhere.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.