Back to skill

Security audit

GraphRAGBuilder

Security checks for vulnerabilities and agentic risk

Overview

The skill has a legitimate docs-to-MCP purpose, but it can automatically install packages, crawl arbitrary URLs, and send or preserve untrusted content with too little containment.

Review carefully before installing. Run only in an isolated virtual environment or container, restrict it to public documentation you trust, avoid authenticated or internal URLs, and review generated server.py and mcp_config.json before adding them to Claude Desktop. Remove or disable automatic package installation and pin dependencies before use on sensitive machines.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/crawl.py:216
Finding

Arbitrary URL Crawling Enables SSRF and External Disclosure of Retrieved Content

Content
View full analysis
tuple[str | None, str]: try: resp = session.get(url, timeout=timeout, allow_redirects=True) resp.raise_for_status() content_type = resp.headers.get("content-type", "") if "text/html" not in content_type and "application/xhtml" not in content_type: return None, f"non-HTML content-type: {content_type}" return resp.text, "ok" except requests.exceptions.Timeout: return None, "timeout" except requests.exceptions.HTTPError as e: return None, f"HTTP {e.response.status_code}" except Exception as e: return None, str(e) ``` ```python page = context.new_page() page.goto(url, timeout=timeout_ms, wait_until="networkidle") page.wait_for_timeout(wait_ms) html = page.content() ``` ```python parser.add_argument("--url", required=True, help="Seed URL to start crawling from") ``` Retrieved content is subsequently incorporated into an Anthropic API request: ```python return f"""Documentation section from: {chunk['page_title']} ({chunk['url']}) Section heading: {chunk['section']} --- Content --- {chunk['text'][:3000]} {codes_text} --- End --- ``` ```python response = client.messages.create( model=model, max_tokens=1024, system=SYSTEM_PROMPT, messages=[{"role": "user", "content": _build_user_prompt(chunk)}], ) ``` ### Technical Analysis The crawler accepts an arbitrary HTTP or HTTPS seed URL and performs requests without resolving and validating the destination address. It does not reject loopback, private, link-local, reserved, or cloud-internal address r ...[truncated 2440 chars]
Remediation
View remediation

other

Error
Location
scripts/extract_concepts.py:249
Finding

Untrusted Website Content Crosses LLM and Persistent MCP Trust Boundaries Without Prompt-Injection Defenses

Content
View full analysis
str: codes_text = "" if chunk["code_examples_raw"]: shown = chunk["code_examples_raw"][:MAX_CODES_IN_PROMPT] codes_text = "\n\nCode examples in this section:\n" + "\n---\n".join( f"```\n{c[:400]}\n```" for c in shown ) return f"""Documentation section from: {chunk['page_title']} ({chunk['url']}) Section heading: {chunk['section']} --- Content --- {chunk['text'][:3000]} {codes_text} --- End --- ``` ```python response = client.messages.create( model=model, max_tokens=1024, system=SYSTEM_PROMPT, messages=[{"role": "user", "content": _build_user_prompt(chunk)}], ) ``` The generated MCP server returns indexed page content directly to the consuming agent: ```python def _tool_search(query: str, n_results: int = 5) -> list[dict]: if _emb_model is None or _chunk_mat is None: return [{"error": "Embedding index not available. Run build_embeddings.py first."}] results = [] for meta, score in _vec_search(query, _emb_model, _chunk_mat, _chunk_idx, n=n_results): chunk_node = G.nodes.get(meta.get("id", ""), {}) text = chunk_node.get("text", "") results.append({ "score": round(score, 3), "section": meta.get("section", ""), "page_title": meta.get("page_title", ""), "url": meta.get("url", ""), "tags": meta.get("tags", ""), "has_code": meta.get("has_code", False), "text": text[:500] + ("..." if len(text) > 500 else ""), }) return results ``` ```python def _too ...[truncated 3401 chars]
Remediation
View remediation

T08 · Insecure Dependencies

Error
Location
scripts/crawl.py:32
Finding

Automatic Installation of Unpinned Dependencies Modifies the Host Environment at Runtime

Content
View full analysis
&1 | tail -5") from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout ``` The generated persistent MCP server contains equivalent behavior: ```python def _install(pkg): print(f"Installing {pkg}...", file=sys.stderr) os.system(f"pip install {pkg} --break-system-packages -q") def _import_ok(mod): try: __import__(mod) return True except ImportError: return False for mod, pkg in [("mcp", "mcp"), ("networkx", "networkx"), ("numpy", "numpy"), ("sentence_transformers", "sentence-transformers")]: if not _import_ok(mod): _install(pkg) ``` Dependencies are not locked to exact versions or hashes: ```text requests>=2.31.0 beautifulsoup4>=4.12.0 lxml>=4.9.0 playwright>=1.40.0 html2text>=2020.1.16 youtube-transcript-ap ...[truncated 2635 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/generate_mcp_server.py:35
Finding

Unsanitized Output Directory Name Is Interpolated into Generated Python and JSON Files

Content
View full analysis
dict: graph_path = input_dir / "graph.json" if not graph_path.exists(): print(f"WARNING: graph.json not found — run build_graph.py first", file=sys.stderr) stats = {"pages": 0, "chunks": 0, "concepts": 0} else: meta = json.loads(graph_path.read_text()).get("meta", {}) stats = meta.get("stats", {}) slug = input_dir.name if slug.endswith("-mcp"): slug = slug[:-4] topic = slug.replace("-", " ").replace("_", " ").title() return { "topic": topic, "slug": slug, "embedding_model": model, "stats": stats, } ``` The value is interpolated into generated Python source, including a triple-quoted docstring: ```python _SERVER_TEMPLATE = '''\ #!/usr/bin/env python3 """ MCP Knowledge Server: {topic} Auto-generated by GraphRAGBuilderSkill. ``` The generated code is written without syntax validation: ```python cfg = _infer_config(input_dir, model) server_code = _SERVER_TEMPLATE.format( topic=cfg["topic"], slug=cfg["slug"], embedding_model=cfg["embedding_model"], ) server_path.write_text(server_code) ``` The same slug is placed into JSON using string formatting: ```python config_json = _CONFIG_TEMPLATE.format( slug=cfg["slug"], server_path=server_path.resolve(), python_cmd=sys.executable, ) config_path.write_text(config_json) ``` ### Technical Analysis The generator derives `slug` from the basename of a user-selected input directory without enforcing a safe character set. Although selected generated constants use Python's representation conversion, `{topic}` and `{slug}` ...[truncated 2545 chars]
Remediation
View remediation
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
Findings (53)

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding

The declared description presents an end-to-end pipeline that turns a website/documentation URL into a runnable MCP knowledge server with semantic search and graph tools. The supplied code chunk is much narrower: it is a graph-construction step only. It reads local extracted JSON outputs, normalizes concepts, constructs a directed multigraph, adds hyperlink edges from raw crawl data, and saves graph.json. While this is consistent with one subcomponent of the broader claimed system ('organizes them into a knowledge graph'), it does not implement most of the headline capabilities in the description. Therefore the description does not accurately represent what this specific code chunk actually does.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The code chunk partially aligns with the declared description only on the crawling portion. Its actual primary function is fetching and storing website pages plus crawl metadata. The declaration, however, describes a much broader end-to-end system that also extracts concepts, builds a knowledge graph, creates embeddings, and outputs a runnable MCP server with 8 tools. None of those downstream capabilities appear in this code chunk. While references in comments/status fields mention later phases like extract/embed/graph, this file itself does not implement them. Therefore the description materially overstates what the supplied code actually does.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
93% confidence
Finding

The declared description covers an end-to-end system that ingests any website/docs URL and outputs a ready-to-install MCP knowledge server with embeddings and tools. The supplied code chunk only implements one intermediate phase: extracting text chunks from locally stored HTML and using Claude to produce structured concept metadata. It assumes prior crawling has already happened (input/raw_content and crawl.json), and it writes extracted JSON for later stages. While this is related to the overall claimed workflow, the chunk’s actual behavior is materially narrower and more specialized than the declared purpose, so the description does not accurately represent what this code chunk actually does.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The declared description presents an end-to-end pipeline that starts from a website/docs URL and produces a complete MCP knowledge server. This code chunk is only the final server-generation stage: it infers config from an existing output folder, writes a templated server.py, and writes an mcp_config.json snippet. The generated server can search/query already-built graph and embedding data, matching the '8 tools' part of the description, but the major upstream capabilities in the declaration—crawling, extraction with Claude, graph building, and embedding generation—are absent from this code. Therefore the description materially overstates what this supplied chunk actually does.

Content

No source excerpt is available for this finding.

os.system() or os exec-family call

High
Category
Dangerous Code Execution
Confidence
95% confidence
Finding

The script automatically executes a shell command to install Python packages at runtime via os.system(). Even though the package names are currently hardcoded, this still causes implicit code execution and network access in the user's environment, expands the attack surface to package-index/supply-chain compromise, and can modify the host system using --break-system-packages without explicit consent.

Content

Scanner excerpt · scripts/build_embeddings.py (reported line 45)May include surrounding context.

python
missing = [pkg for mod, pkg in deps.items() if not _import_ok(mod)]
    if missing:
        print(f"Installing: {', '.join(missing)}...")
        os.system(f"pip install {' '.join(missing)} --break-system-packages -q")

def _import_ok(mod):
    try:

os.system() or os exec-family call

High
Category
Dangerous Code Execution
Confidence
96% confidence
Finding

The script executes a shell command to install packages at runtime using os.system("pip install ... --break-system-packages"). Even though the current package list is hardcoded, this still introduces unaudited network access, arbitrary environment modification, PATH/pip hijacking risk, and can damage the host Python environment; in an agent skill that may run automatically, this is especially unsafe.

Content

Scanner excerpt · scripts/build_graph.py (reported line 45)May include surrounding context.

python
missing = [pkg for mod, pkg in deps.items() if not _import_ok(mod)]
    if missing:
        print(f"Installing: {', '.join(missing)}")
        os.system(f"pip install {' '.join(missing)} --break-system-packages -q")

def _import_ok(mod):
    try:

os.system() or os exec-family call

High
Category
Dangerous Code Execution
Confidence
91% confidence
Finding

The script automatically executes a shell command to install Python packages at runtime, which modifies the host environment without explicit user consent. Although the package names are hardcoded and not directly attacker-controlled here, this still creates a supply-chain and environment-integrity risk, especially because it may pull and execute unpinned packages from PyPI during normal use.

Content

Scanner excerpt · scripts/crawl.py (reported line 50)May include surrounding context.

python
if missing:
        print(f"Installing missing packages: {', '.join(missing)}")
        os.system(f"pip install {' '.join(missing)} --break-system-packages -q")
        print()

_ensure_deps()

os.system() or os exec-family call

High
Category
Dangerous Code Execution
Confidence
95% confidence
Finding

The Playwright fallback installs a package at runtime by invoking pip through the shell, again changing the system automatically and introducing supply-chain exposure. In a crawler that fetches arbitrary websites, this is especially risky because normal feature use can trigger browser support installation on systems that did not intend to allow package management side effects.

Content

Scanner excerpt · scripts/crawl.py (reported line 246)May include surrounding context.

python
from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout
    except ImportError:
        print("     Installing playwright...")
        os.system("pip install playwright --break-system-packages -q")
        os.system("playwright install chromium --with-deps 2>&1 | tail -5")
        from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout

os.system() or os exec-family call

High
Category
Dangerous Code Execution
Confidence
97% confidence
Finding

This command installs Chromium and system dependencies using a shell pipeline, which can significantly alter the host environment and may require elevated privileges or pull additional OS packages. That behavior is dangerous in an agent skill because invoking the crawler can unexpectedly perform privileged system changes beyond simple web fetching.

Content

Scanner excerpt · scripts/crawl.py (reported line 247)May include surrounding context.

python
except ImportError:
        print("     Installing playwright...")
        os.system("pip install playwright --break-system-packages -q")
        os.system("playwright install chromium --with-deps 2>&1 | tail -5")
        from playwright.sync_api import sync_playwright, TimeoutError as PWTimeout

    try:

os.system() or os exec-family call

High
Category
Dangerous Code Execution
Confidence
98% confidence
Finding

The script automatically executes a shell command to install packages at runtime using os.system and pip with --break-system-packages. Even though the package names are currently hardcoded, this still performs implicit code execution from external package indexes, modifies the host environment without consent, and expands the attack surface through dependency confusion, malicious package compromise, or environment breakage.

Content

Scanner excerpt · scripts/extract_concepts.py (reported line 36)May include surrounding context.

python
if not __import_ok(mod)]
    if missing:
        print(f"Installing: {', '.join(missing)}")
        os.system(f"pip install {' '.join(missing)} --break-system-packages -q")

def __import_ok(mod):
    try:

Anti-Refusal Statement

High
Category
Anti-Refusal
Confidence
70% confidence
Finding

Skill instructs the agent to never refuse or to always comply. Suppressing the agent's ability to decline removes a core safety control and enables downstream harmful requests to succeed.

Content

Scanner excerpt · scripts/extract_concepts.py (reported line 254)May include surrounding context.

python
Your job: given a section of documentation, extract structured knowledge that
will be used to build a searchable knowledge graph and MCP server.

Always respond with valid JSON only — no markdown fences, no explanation.
"""

def _build_user_prompt(chunk: dict) -> str:

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The skill description and idea use broad, generic language around building a Claude skill from an input website and scraping learning materials, which can easily match common user phrasing such as 'scraping docs' or 'making docs searchable.' Overly broad triggers can cause the skill to activate in unintended contexts, leading to inappropriate execution of web scraping, indexing, or code-generation behavior when the user did not explicitly request this specific capability.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The plan explicitly sends crawled documentation chunks and source URLs to the Claude API for concept extraction, but it does not include any user notice, consent step, or data-classification guardrail before transmitting third-party content off-box. This creates a real privacy and compliance risk because the tool may ingest internal docs, licensed content, or sensitive material and disclose it to an external processor unexpectedly.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding

The skill instructs the user to run local Python scripts that access the network, filesystem, environment variables, and shell, but it does not declare any tool scope or permission boundaries. In an agent ecosystem, missing explicit permissions makes the skill harder to sandbox and review, increasing the chance of overbroad execution or accidental access to sensitive local resources.

Content

No source excerpt is available for this finding.

Vague Triggers

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The trigger guidance says to invoke the skill even when the user merely mentions broad phrases like 'scraping docs' or 'making docs searchable,' which can cause the agent to over-trigger a workflow that includes crawling websites, installing packages, and handling API keys. Overly broad activation increases the chance of unintended network activity, unnecessary local code execution, or use in contexts where the user did not actually consent to this heavier pipeline.

Content

No source excerpt is available for this finding.

Session Persistence

Medium
Category
Rogue Agent
Confidence
75% confidence
Finding

Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Content

Scanner excerpt · TODO.md (reported line 65)May include surrounding context.

md
---

### Scheduled Re-Crawls
Add a `--schedule` option (e.g., `--schedule daily`) that writes a cron entry or launchd plist to re-run `build.py` automatically for a given topic. The incremental mode means re-runs are cheap if the site hasn't changed.

---

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The script performs automatic dependency installation even though its stated purpose is embedding-index construction. In a skill that processes arbitrary websites/docs and is meant to be runnable by users, hidden installation behavior is risky because it triggers unreviewed package retrieval and execution paths that are outside the core task.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The script initiates package installation and later downloads a model on first use, creating unannounced network activity and supply-chain exposure. This is more concerning in the skill context because the tool is presented as a turnkey builder that users may run on their own machines, so implicit downloads can violate operator expectations and security controls.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

Automatically installing Python packages at runtime is outside the minimal needs of graph construction and creates implicit network and code-execution behavior. In the context of an MCP-building skill that processes external content and may be run by users or agents with broad filesystem access, hidden dependency installation increases supply-chain exposure and makes execution less predictable and less auditable.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The script modifies the Python environment without explicit warning or consent, and does so through a shell command that reaches out to package indexes. This is dangerous because users may execute the tool expecting local graph processing only, while the skill silently performs network activity and changes system packages, including use of --break-system-packages which can interfere with host-managed Python installations.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The crawler bootstraps missing dependencies by executing shell commands during runtime, which silently mutates the environment and trusts remote package sources. This is not necessarily malicious, but it is unsafe operationally and increases the blast radius of using the skill on sensitive hosts.

Content

No source excerpt is available for this finding.

Dynamic import via __import__()

Medium
Category
Dangerous Code Execution
Confidence
75% confidence
Finding

Dynamic import() can load arbitrary modules at runtime, bypassing static analysis and potentially importing malicious code.

Content

Scanner excerpt · scripts/crawl.py (reported line 44)May include surrounding context.

python
missing = []
    for module, package in deps.items():
        try:
            __import__(module)
        except ImportError:
            missing.append(package)

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
93% confidence
Finding

The script performs shell-based package installation without an explicit upfront warning or consent flow, violating the principle of least astonishment for code execution and system modification. While this is largely a trust and safety issue rather than a classic exploit, it is still dangerous in agent contexts where users may not realize execution changes the host.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The Playwright fallback not only installs a Python package but also downloads browser binaries and system dependencies at runtime. In the context of an autonomous documentation crawler, this hidden installation path is more dangerous because visiting JS-heavy sites can automatically trigger substantial host changes.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

Automatic Playwright installation and browser setup happen without prior disclosure, and the browser install may also bring in system packages. This hidden behavior is especially concerning for an agent skill because an apparently simple crawl request can lead to substantial software installation on the host.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.