Back to skill

Security audit

A research is searching around something.

Security checks for vulnerabilities and agentic risk

Overview

This research skill is not clearly malicious, but it needs review because it fetches arbitrary web pages with weak boundaries and sends fetched content to an external LLM.

Install only if you are comfortable with a research tool that performs broad web retrieval and sends fetched paper and page text to OpenRouter. Avoid using it with private topics, internal URLs, proprietary documents, or sensitive data unless the fetch scope, redirects, cache location, and dependency versions are tightened.

Vulnerability Patterns
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • 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 (4)

T01 · Skill Instruction Hijacking

Error
Location
scripts/owl.py:412
Finding

Untrusted Web Content Is Inserted Directly into the LLM Prompt

Content
View full analysis
str: parts = [ f"You are OWL, an expert research analyst.\n", f"Topic: {query}\n", "=" * 70, "PAPERS (title + abstract + intro + full text where available)", "=" * 70, ] for i, p in enumerate(papers, 1): authors = ", ".join(p.get("authors", [])[:4]) parts.append(f"\n[P{i}] {p['title']}") parts.append(f"Authors : {authors} | Published: {p.get('published','')}") parts.append(f"URL : {p.get('url','')}") parts.append(f"PDF : {p.get('pdf_url','')}") full = p.get("full_text", "") if full: parts.append(f"\nFull text (from PDF via markitdown):\n{full}") else: parts.append(f"\nAbstract:\n{p.get('abstract','')}") parts.append("-" * 60) parts += ["\n" + "=" * 70, "WEB SEARCH RESULTS", "=" * 70] for search_type, results in web_results.items(): parts.append(f"\n── {search_type.upper()} SEARCH ──") for r in results: parts.append(f"\n[{r['label']}] {r['title']}") if r.get("url"): parts.append(f"URL : {r['url']}") if r.get("snippet"): parts.append(f"Snippet: {r['snippet']}") if r.get("page_content"): parts.append(f"Content:\n{r['page_content']}") parts.append("") ``` ```python for r in results[:2]: if r.get("url"): content = web_fetcher.quick_fetch(r["url"]) if content: r["page_content"] = content[:4000] ``` ```python prompt = build_summary_prompt(query, best, web_results) summary = call_claude(prompt, None, max_tokens ...[truncated 2286 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/web_fetcher.py:450
Finding

Unrestricted Search-Result Fetching Permits SSRF Through Direct URLs and Redirects

Content
View full analysis
FetchResult: retries = retries if retries is not None else self.max_retries timeout = timeout or self.timeout request_headers = self._get_headers(url) if headers: request_headers.update(headers) current_proxy = proxy or self._get_current_proxy() self.token_bucket.acquire() start_time = time.time() for attempt in range(retries): try: with self._get_session() as session: for key, value in request_headers.items(): session.headers[key] = value if method.upper() == "POST": response = session.post( url, data=data, proxies={"http": current_proxy, "https": current_proxy} if current_proxy else None, timeout=timeout, allow_redirects=self.follow_redirects, ) else: response = session.get( url, params=data, proxies={"http": current_proxy, "https": current_proxy} if current_proxy else None, timeout=timeout, allow_redirects=self.follow_redirects, ) elapsed = time.time() - start_time cookies = {} if hasattr(response, 'cookies'): cookies = {k: v for k, v in response.cookies.items()} content = response.text text = self._ ...[truncated 2609 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/owl.py:111
Finding

Predictable Shared Temporary Cache Permits Symlink Attacks and Research Cache Poisoning

Content
View full analysis
str: import subprocess os.makedirs(tmp_dir, exist_ok=True) safe_id = re.sub(r"[^\w.-]", "_", arxiv_id) pdf_path = os.path.join(tmp_dir, f"{safe_id}.pdf") md_path = os.path.join(tmp_dir, f"{safe_id}.md") if not os.path.exists(pdf_path): try: req = urllib.request.Request(pdf_url, headers={"User-Agent": "owl/3.0"}) with urllib.request.urlopen(req, timeout=30) as resp, \ open(pdf_path, "wb") as fout: fout.write(resp.read()) except Exception as e: err(f"download failed for {arxiv_id}: {e}") return "" if not os.path.exists(md_path): try: from markitdown import MarkItDown result = MarkItDown().convert(pdf_path) text = result.text_content or "" with open(md_path, "w", encoding="utf-8") as f: f.write(text) ``` ```python def pdf_to_markdown_full(arxiv_id: str, tmp_dir: str = "/tmp/owl_papers", max_chars: int = 40_000) -> str: safe_id = re.sub(r'[^\w.-]', '_', arxiv_id) md_path = os.path.join(tmp_dir, f"{safe_id}.md") if not os.path.exists(md_path): return "" try: with open(md_path, "r", encoding="utf-8", errors="ignore") as f: text = f.read() ``` ### Technical Analysis The default cache path is the predictable shared location `/tmp/owl_papers`. Cache filenames are deterministically derived from public arXiv identifiers. Files are opened using normal path-foll ...[truncated 2056 chars]
Remediation
View remediation

T08 · Insecure Dependencies

Warning
Location
SKILL.md:43
Finding

Unpinned Third-Party Package Installation Creates Supply-Chain Exposure

Content
View full analysis
Remediation
View remediation
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • 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
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (20)

Tainted flow: 'api_key' from os.environ.get (line 327, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/owl.py (reported line 331)May include surrounding context.

python
if not api_key:
        raise EnvironmentError("No API key provided and OPENROUTER_API_KEY is not set.")

    response = requests.post(
        url="https://openrouter.ai/api/v1/chat/completions",
        headers={
            "Authorization": f"Bearer {api_key}",

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

A description-behavior mismatch is dangerous because users and orchestration layers may trust the declared purpose while the implementation performs materially different actions, including undeclared web scraping with stealth headers, browser impersonation, proxy support, and retry logic. Those hidden behaviors expand the attack surface, can evade monitoring or policy controls, and may enable covert data exfiltration or unauthorized collection under the guise of benign research.

Content

No source excerpt is available for this finding.

Ssd 1

High
Category
Not specified by scanner
Confidence
98% confidence
Finding

The final summarization prompt concatenates raw full paper text and fetched web page content from multiple untrusted sources into a single high-privilege instruction context. This creates a broad prompt-injection surface where hostile content can override summarization rules, fabricate citations, embed tracking or exfiltration prompts, or steer the model into unsafe or misleading output; in a research skill, this directly undermines result integrity and can leak sensitive fetched material to the provider.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

High
Category
Not specified by scanner
Confidence
98% confidence
Finding

The convenience helpers are internally inconsistent: quick_fetch is documented to return text and actually returns a string, but fetch_and_parse treats that return value as a FetchResult and accesses is_success and get_text_content. This can cause runtime failures or silent misbehavior in higher-level automation, which is dangerous in an agent skill because broken control flow may suppress safety checks, error handling, or provenance guarantees during research execution.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding

The skill declares significant capabilities via required environment variables, shell-based install/run steps, network access, and filesystem caching, but does not define any explicit tool scope or permission boundaries. That creates an over-privileged execution surface where a caller or platform may permit broader file, shell, and network actions than users would reasonably expect from a research-summary skill.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

The trigger phrases are broad enough to match many normal informational requests such as 'what is X' or 'deep dive', which can cause the skill to activate unexpectedly. Because this skill sends queries and fetched content to external services and performs network-heavy actions, accidental activation increases the risk of unintended data disclosure, unnecessary external calls, and user-surprising behavior.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The skill explicitly states that it passes paper content and web results to external LLM/API services, but it does not prominently warn users that their query and fetched document content will be transmitted to third parties. This creates a privacy and compliance risk, especially if the user query or retrieved documents contain sensitive, proprietary, or regulated information.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
98% confidence
Finding

The module docstring says OPENROUTER_API_KEY is for OPENROUTER.AI while later CLI/environment documentation says ANTHROPIC_API_KEY is required, and the implementation actually ignores the function key argument and reads OPENROUTER_API_KEY directly. This creates a direct mismatch between documentation and runtime behavior that can mislead users about which service is contacted and which credential is used.

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/owl.py (reported line 148)May include surrounding context.

python
f.write(text)
        except ImportError:
            try:
                proc = subprocess.run(
                    ["markitdown", pdf_path, "-o", md_path],
                    capture_output=True, text=True, timeout=60,
                )

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
85% confidence
Finding

When the Python markitdown library is unavailable, the code falls back to invoking a local markitdown executable via subprocess.run. Spawning local executables is a broader capability than the manifest's described research behavior and is not explicitly justified in the skill description, which frames markitdown as a document conversion step rather than arbitrary local process execution.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The docstring for select_best_papers claims it resolves the best available model via the Anthropic API, but the actual resolution call is commented out and replaced with a fixed model string. This is an active contradiction between documented intent and real behavior, especially since the surrounding code also routes requests through OpenRouter rather than Anthropic.

Content

No source excerpt is available for this finding.

Ssd 1

Medium
Category
Not specified by scanner
Confidence
96% confidence
Finding

Untrusted paper text is embedded directly into the model-selection prompt with no delimiting instructions that the material is data, not instructions. A malicious paper or converted PDF could include prompt-injection text that biases selection, causes the model to ignore the requested output schema, or manipulates downstream reasoning, reducing integrity of the research output.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The tool sends aggregated paper text and fetched web content to a third-party LLM service for selection and summarization without an explicit runtime warning or consent step. In a research workflow, source material may include proprietary notes, sensitive URLs, or licensed content, so silent transmission expands data exposure beyond user expectations.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The module-level documentation is entirely in Chinese, presenting the skill as Chinese-only rather than offering a language choice. Under the stated policy, forcing a specific language or locale without user opt-in is a natural-language policy violation.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The module explicitly advertises stealth headers, browser impersonation, anti-bot evasion, and proxy support, which materially expands capability beyond a read-only research summarization tool. In this skill context, those features can be used to bypass site access controls or scrape targets in ways the user did not request, increasing abuse potential and making the component riskier than its stated purpose requires.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
87% confidence
Finding

The fetcher supports arbitrary POST requests even though the skill is described as performing research, reading papers, and summarizing content. Write-capable HTTP methods expand the attack surface from passive retrieval to interaction with third-party services, enabling unintended actions such as form submissions, API mutations, or credential-bearing requests if misused by upstream prompts or tools.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
83% confidence
Finding

Downloaded PDFs and converted markdown are stored under /tmp by default, which may leave recoverable research material on shared systems and persists sensitive content longer than users expect. In this skill's context, the papers are usually public, but queries, selected materials, or non-public URLs in derived content could still create confidentiality or hygiene issues.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
76% confidence
Finding

If the Python library import fails, the script falls back to executing the external 'markitdown' command via subprocess. This behavior is not clearly disclosed to the user during execution, even though invoking an external binary is a safety-relevant operation.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
71% confidence
Finding

Several inline comments in the fetch_page function are written only in Chinese, such as the gzip, encoding-detection, HTML-tag removal, and whitespace-cleanup notes. This creates a language-specific instruction context without any opt-in or documented locale justification, which can conflict with organizational language policy expectations.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
88% confidence
Finding

The example and runtime-facing print strings in the main block are hardcoded in Chinese, which enforces a specific language for users running the script. The file does not indicate that this locale choice is optional or region-specific.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.