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

Content
View full analysis

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
View 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

Content
View full analysis

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
View 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

Content
View full analysis

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
View 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.

Content

No source excerpt is available for this finding.

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.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
70% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · README.md (reported line 54)May include surrounding context.

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

text

## Quick Start

Autonomous Decision Making

Medium
Category
Excessive Agency
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.

Content

Scanner excerpt · README.md (reported line 558)May include surrounding context.

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

## Performance

Undeclared Tool Scope

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.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
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.

Content

Scanner excerpt · lib/scraper.py (reported line 128)May include surrounding context.

python
"Content-Type": "application/json"
            }
            
            resp = requests.post(
                f"{self.gateway_url}/tools/invoke",
                json=open_payload,
                headers=headers,

External Transmission

Medium
Category
Data Exfiltration
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.

Content

Scanner excerpt · lib/scraper.py (reported line 153)May include surrounding context.

python
}
            }
            
            resp = requests.post(
                f"{self.gateway_url}/tools/invoke",
                json=snapshot_payload,
                headers=headers,

Tainted flow: 'snapshot_payload' from requests.post (line 145, network input) → requests.post (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 · lib/scraper.py (reported line 153)May include surrounding context.

python
}
            }
            
            resp = requests.post(
                f"{self.gateway_url}/tools/invoke",
                json=snapshot_payload,
                headers=headers,

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.

Content

No source excerpt is available for this finding.

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.

Content

No source excerpt is available for this finding.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Confidence
70% confidence
Finding

Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Content

Scanner excerpt · setup.sh (reported line 78)May include surrounding context.

sh
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

Missing User Warnings

Low
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
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.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.