Back to skill

Security audit

anydocs - Generic Documentation Indexing & Search

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its documentation-search purpose, but it has under-scoped network, gateway-token, and cache-file behaviors that need review before installation.

Use this only with trusted documentation URLs and a trusted local gateway. Avoid --use-browser with any non-loopback gateway, do not pass tokens inline on shared systems, update dependencies before use, use a virtual environment, and avoid indexing sensitive internal docs unless local cache retention is acceptable.

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
lib/config.py:52
Finding
Arbitrary Network Access and Server-Side Request Forgery## Vulnerability Details **File Location**: `lib/config.py:52-68` **Related Locations**: `lib/scraper.py:70-72`, `cli.py:259-274` **Vulnerability Type**: Server-Side Request Forgery (SSRF) **Risk Level**: High ### Vulnerable Code ```python # lib/config.py:52-68 # Validate inputs if not name.strip(): raise ValueError("Profile name cannot be empty") if not base_url.strip(): raise ValueError("base_url cannot be empty") if not sitemap_url.strip(): raise ValueError("sitemap_url cannot be empty") if search_method not in ["keyword", "semantic", "hybrid"]: raise ValueError("search_method must be 'keyword', 'semantic', or 'hybrid'") self.configs[name] = { "name": name, "base_url": base_url.rstrip("/"), "sitemap_url": sitemap_url, "search_method": search_method, "cache_ttl_days": cache_ttl_days, } ``` ```python # lib/scraper.py:70-72 try: resp = self.session.get(url, timeout=10) resp.raise_for_status() ``` ```python # cli.py:259-274 # Build full URL if path doesn't start with http if path.startswith("http"): url = path else: url = f"{cfg['base_url']}/{path.lstrip('/')}" # Try cache first cached_page = config_obj.cache_mgr.get_page(url, cfg["cache_ttl_days"]) if cached_page: click.echo(f"Title: {cached_page['title']}\n") click.echo(cached_page['content']) return # Fetch fresh click.echo(f"Fetching {url}...") engine = DiscoveryEngine(cfg['base_url'], cfg['sitemap_url']) page = engine.scrape_page(url) ``` ### Technical Analysis Profile validation only verifies that URL strings are nonempty. It does not parse or constrain the URL scheme, hostname, port, embedded credentials, or resolved IP address. The `fetch` command also treats any value beginning with `http` as a directly fetchable URL. Sitemap entries are subsequently passed to the same unrestricted HTTP client. Consequently, a malicious sitemap ...[truncated 1847 chars]
Remediation
## Remediation Suggestions 1. Parse URLs with `urllib.parse.urlsplit()` and permit only explicitly supported schemes, preferably `https`. 2. Reject URLs containing user information, malformed hosts, fragments where inappropriate, and nonstandard ports unless explicitly approved. 3. Resolve hostnames before every request and reject loopback, private, link-local, multicast, unspecified, and reserved IP ranges for both IPv4 and IPv6. 4. Explicitly block cloud metadata destinations, including `169.254.169.254`, even when reached through DNS aliases. 5. Require all sitemap entries and crawled pages to match the configured documentation origin or a per-profile hostname allowlist. 6. Disable automatic redirects or validate every redirect destination using the same scheme, origin, and resolved-address policy. 7. Replace string-prefix origin checks with normalized scheme/hostname/port comparisons. 8. Apply response-size and content-type limits to prevent memory or disk exhaustion. 9. Update the documentation so security claims accurately reflect the controls implemented.

T09 · Insecure Skill Coding Practices

Error
Location
lib/scraper.py:123
Finding
OpenClaw Gateway Bearer Token Can Be Exfiltrated to an Arbitrary Endpoint## Vulnerability Details **File Location**: `lib/scraper.py:123-133` **Related Locations**: `lib/scraper.py:50-51`, `cli.py:97-98`, `cli.py:132-143` **Vulnerability Type**: Credential Exfiltration Through an Unvalidated Gateway URL **Risk Level**: High ### Vulnerable Code ```python # cli.py:132-143 # Get gateway token from env if not provided if use_browser and not gateway_token: import os gateway_token = os.environ.get("OPENCLAW_GATEWAY_TOKEN", "") # Scrape engine = DiscoveryEngine( cfg["base_url"], cfg["sitemap_url"], use_browser=use_browser, gateway_url=gateway_url or "http://127.0.0.1:18789", gateway_token=gateway_token ) ``` ```python # lib/scraper.py:50-51 self.gateway_url = gateway_url or "http://127.0.0.1:18789" self.gateway_token = gateway_token or "" ``` ```python # lib/scraper.py:123-133 headers = { "Authorization": f"Bearer {self.gateway_token}", "Content-Type": "application/json" } resp = requests.post( f"{self.gateway_url}/tools/invoke", json=open_payload, headers=headers, timeout=30 ) ``` ### Technical Analysis The CLI accepts an unrestricted `--gateway-url`. When browser rendering is enabled, it obtains the gateway token either from a command-line option or the `OPENCLAW_GATEWAY_TOKEN` environment variable. The scraper then unconditionally sends that token in an `Authorization: Bearer` header to the supplied gateway URL. The HTTPS checks in `DiscoveryEngine.__init__()` apply only to the documentation `base_url` and `sitemap_url`; they do not validate the destination receiving the bearer token. Therefore, the token can be sent to an arbitrary host. Plaintext HTTP is also allowed, exposing the credential to network interception when a non-loopback HTTP gateway is used. This behavior is unnecessary for the declared default workflow, which documents a local OpenClaw gateway at `127.0.0.1`. Allowing u ...[truncated 1381 chars]
Remediation
## Remediation Suggestions 1. Restrict gateway URLs to loopback addresses by default, including normalized `localhost`, `127.0.0.0/8`, and `::1`. 2. Require an explicit trusted-host allowlist before permitting a remote gateway. 3. Require HTTPS with certificate verification for every non-loopback gateway. 4. Resolve the gateway hostname and reject private, link-local, or unexpected addresses unless they are explicitly configured as trusted. 5. Disable redirects for authenticated gateway requests, or strip authorization and revalidate the destination before following any redirect. 6. Bind the token to the expected gateway origin where the authentication system supports audience or scope restrictions. 7. Prefer a protected credential provider or environment variable over command-line token arguments, which may be exposed through process listings or shell history. 8. Validate the gateway URL once during initialization and fail closed before any authenticated request. 9. Avoid including gateway response bodies in logs when they could contain sensitive information.

T09 · Insecure Skill Coding Practices

Warning
Location
lib/cache.py:88
Finding
Profile Name Path Traversal Enables Arbitrary JSON File Overwrite or Deletion## Vulnerability Details **File Location**: `lib/cache.py:88-91` **Related Locations**: `lib/config.py:52-68`, `lib/cache.py:94-96`, `lib/cache.py:119-129` **Vulnerability Type**: Path Traversal **Risk Level**: Medium ### Vulnerable Code ```python # lib/config.py:52-68 # Validate inputs if not name.strip(): raise ValueError("Profile name cannot be empty") if not base_url.strip(): raise ValueError("base_url cannot be empty") if not sitemap_url.strip(): raise ValueError("sitemap_url cannot be empty") if search_method not in ["keyword", "semantic", "hybrid"]: raise ValueError("search_method must be 'keyword', 'semantic', or 'hybrid'") self.configs[name] = { "name": name, "base_url": base_url.rstrip("/"), "sitemap_url": sitemap_url, "search_method": search_method, "cache_ttl_days": cache_ttl_days, } ``` ```python # lib/cache.py:88-91 index_file = self.index_dir / f"{profile}_index.json" index_data["indexed_at"] = datetime.now().isoformat() with open(index_file, "w") as f: json.dump(index_data, f, indent=2) ``` ```python # lib/cache.py:119-129 if profile: # Clear specific profile index index_file = self.index_dir / f"{profile}_index.json" if index_file.exists(): index_file.unlink() deleted += 1 ``` ### Technical Analysis Profile names are accepted as long as they are nonempty. They may therefore contain `/`, `..`, or absolute-path-like components. The cache manager interpolates the profile directly into a filesystem path without normalization or containment verification. `pathlib.Path` resolves traversal components according to normal filesystem semantics. A crafted profile can consequently make `index_file` reference a path outside `~/.anydocs/cache/indexes`. Index creation opens the computed path in write mode, truncating an existing file before writing JSON. Profile-specific cache clearing calls `u ...[truncated 1440 chars]
Remediation
## Remediation Suggestions 1. Restrict profile names to a safe identifier format, for example: ```python if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_-]{0,63}", name): raise ValueError("Invalid profile name") ``` 2. Reject path separators, `.` and `..` components, control characters, and absolute paths. 3. Resolve every generated cache path and verify it remains a child of the resolved `index_dir`: ```python candidate = (self.index_dir / f"{profile}_index.json").resolve() candidate.relative_to(self.index_dir.resolve()) ``` 4. Centralize safe index-path generation so save, load, expiry, and deletion operations enforce the same policy. 5. Use atomic writes through a temporary file created inside `index_dir`, followed by `os.replace()`. 6. Create configuration and cache files with restrictive user-only permissions where sensitive internal documentation may be stored. 7. Validate existing profile names while loading configuration to prevent manually modified legacy configuration from bypassing new checks.
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
Findings (15)

Known Vulnerable Dependency: click==8.1.7 — 1 advisory(ies): CVE-2026-7246 (Pallets Click, versions 8.3.2 and below, contain a command injection vulnerabili)

High
Category
Supply Chain
Confidence
80% confidence
Finding
The dependency click==8.1.7 is flagged with a command injection advisory, and CLI frameworks can become dangerous when application code passes untrusted input into shell-like execution paths. In a documentation indexing/search skill, user-controlled URLs, paths, or query terms may flow through command-line handling or downstream subprocess wrappers, so keeping a vulnerable CLI dependency increases risk even if exploitability depends on application usage.

Known Vulnerable Dependency: lxml==4.9.3 — 2 advisory(ies): CVE-2026-41066 (lxml: Default configuration of iterparse() and ETCompatXMLParser() allows XXE to); CVE-2026-41066 (lxml is a library for processing XML and HTML in the Python language. Prior to 6)

High
Category
Supply Chain
Confidence
90% confidence
Finding
lxml==4.9.3 is flagged for XXE-related issues in default XML parser configurations. Since this skill processes remote documentation content and may encounter XML-based formats such as sitemaps, feeds, or structured docs metadata, vulnerable parsing could allow external entity expansion, leading to file disclosure, SSRF, or unexpected network access.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The documentation promotes browser-based indexing through an external gateway using a gateway token, but it does not disclose that page contents, URLs, and possibly authenticated or internal documentation data may be transmitted to a third-party service. This can lead users to unintentionally expose sensitive documentation content or mishandle credentials because the trust boundary and data flow are not clearly communicated.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
export PATH="/path/to/skills/anydocs:$PATH"

# Or create a symlink (requires the venv to be active)
sudo ln -s /path/to/skills/anydocs/anydocs.py /usr/local/bin/anydocs
```

## Quick Start
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Autonomous Decision Making

Medium
Category
Excessive Agency
Content
**TTL Behavior:**
- Default: 7 days
- Configurable per profile
- Expired caches are automatically removed on access
- Manual cleanup: `anydocs cache clear`

## Performance
Confidence
80% confidence
Finding
Skill enables autonomous high-impact decisions without human-in-the-loop verification. Critical operations (destructive commands, financial transactions, data deletion) should require explicit user confirmation.

Lp3

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding
The skill exposes capabilities that imply network access, local file reads/writes, environment access, and shell execution, but the manifest does not declare any tool scope or restrictions. In a skill that fetches remote documentation, caches content under the user's home directory, and invokes installation/CLI flows, this omission weakens least-privilege controls and can let downstream agents use broader powers than users expect.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The markdown shows `cache clear` commands under 'Clear Cache' but does not warn users that this operation removes cached pages/indexes and may be irreversible for local data. Under the markdown-file criteria for SQP-2, destructive operations should include a user warning when they can affect user data or system state.

External Transmission

Medium
Category
Data Exfiltration
Content
"Content-Type": "application/json"
            }
            
            resp = requests.post(
                f"{self.gateway_url}/tools/invoke",
                json=open_payload,
                headers=headers,
Confidence
82% confidence
Finding
The code transmits arbitrary user-supplied URLs to a configurable gateway service along with a bearer token, which can cause server-side requests and browser actions against attacker-chosen destinations. If gateway_url is misconfigured to a non-local or untrusted service, sensitive browsing targets, auth material, or internal network reachability could be exposed, making this an SSRF/trust-boundary issue.

External Transmission

Medium
Category
Data Exfiltration
Content
}
            }
            
            resp = requests.post(
                f"{self.gateway_url}/tools/invoke",
                json=snapshot_payload,
                headers=headers,
Confidence
81% confidence
Finding
This is the same trust-boundary problem repeated for the snapshot action: the code sends authenticated requests to a configurable gateway and then accepts returned content as page data. In a hostile or misconfigured environment, an attacker-controlled gateway could receive the token, service requests for arbitrary targets, and return untrusted content that is later processed or stored.

Tainted flow: 'snapshot_payload' from requests.post (line 145, network input) → requests.post (network output)

Medium
Category
Data Flow
Content
}
            }
            
            resp = requests.post(
                f"{self.gateway_url}/tools/invoke",
                json=snapshot_payload,
                headers=headers,
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.

Known Vulnerable Dependency: requests==2.31.0 — 6 advisory(ies): CVE-2024-47081 (Requests vulnerable to .netrc credentials leak via malicious URLs); CVE-2024-35195 (Requests `Session` object does not verify requests after making first request wi); CVE-2026-25645 (Requests has Insecure Temp File Reuse in its extract_zipped_paths() utility func) +3 more

Medium
Category
Supply Chain
Confidence
97% confidence
Finding
The skill pins requests==2.31.0, which has multiple published advisories including credential leakage via malicious URLs and request verification flaws. Because this skill indexes arbitrary documentation sites, it is likely to make outbound HTTP requests to untrusted targets, which increases exposure to SSRF-adjacent behaviors, credential leakage, or mishandled TLS/session validation if vulnerable code paths are reached.

Known Vulnerable Dependency: python-dotenv==1.0.0 — 2 advisory(ies): CVE-2026-28684 (python-dotenv: Symlink following in set_key allows arbitrary file overwrite via ); CVE-2026-28684 (python-dotenv reads key-value pairs from a .env file and can set them as environ)

Medium
Category
Supply Chain
Confidence
80% confidence
Finding
python-dotenv==1.0.0 is associated with symlink-following and arbitrary file overwrite issues in set_key-related functionality. If this skill ever writes or updates .env files during setup, configuration, or automation, a local attacker or malicious workspace state could abuse symlinks to overwrite unintended files.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
if [ "$1" == "--system" ] || [ "$2" == "--system" ]; then
    echo ""
    echo "Installing system-wide symlink..."
    sudo ln -sf "$(pwd)/anydocs.py" /usr/local/bin/anydocs
    echo "✓ anydocs available as 'anydocs' command (requires 'venv' activation)"
fi
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Missing User Warnings

Low
Confidence
95% confidence
Finding
The README shows `--gateway-token YOUR_TOKEN` directly in example commands, which encourages users to place secrets on the command line where they may be exposed via shell history, process listings, logging, or copy/paste into shared terminals. Although the document later warns against this practice elsewhere, the unsafe example appears at the point of use and can still lead to credential leakage.

Missing User Warnings

Low
Confidence
91% confidence
Finding
The skill states that fetched pages and indexes are cached locally, including under ~/.anydocs/cache and configuration under ~/.anydocs/config.json, but it does not clearly warn users that remote documentation content may be stored persistently on disk. In contexts involving internal or sensitive documentation, this can create unintended data retention and local exposure risks, especially on shared systems or developer workstations.

Static analysis

No suspicious patterns detected.