Back to skill

Security audit

Web Search Plus

Security checks for vulnerabilities and agentic risk

Overview

This web-search skill is mostly purpose-aligned, but it needs Review because some configuration paths can expose API keys beyond the declared provider-host and cache-only boundaries.

Install only if you are comfortable with a Review-level credential-handling risk. Prefer environment variables over the setup wizard's config.json for API keys, avoid custom provider endpoint overrides unless you control the host, disable or clear caching for sensitive searches, and use explicit providers or self-hosted SearXNG when queries or URLs are sensitive.

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/search.py:2196
Finding
Authenticated Provider Requests Can Be Redirected to Arbitrary Endpoints<![CDATA[ ## Vulnerability Details **File Location**: `scripts/search.py:2196-2247`, `scripts/search.py:3237-3247`, `scripts/search.py:3574-3595`, `scripts/search.py:3628-3686`, `config.example.json:36-39`, `config.example.json:42-47`, `config.example.json:55-62`, `config.example.json:74-86` **Vulnerability Type**: Unvalidated authenticated endpoint override, credential disclosure, and server-side request forgery **Risk Level**: High ### Vulnerable Code ```python def search_querit( query: str, api_key: str, max_results: int = 5, language: str = "en", country: str = "us", time_range: Optional[str] = None, include_domains: Optional[List[str]] = None, exclude_domains: Optional[List[str]] = None, base_url: str = "https://api.querit.ai", base_path: str = "/v1/search", timeout: int = 30, ) -> dict: endpoint = base_url.rstrip("/") + base_path body: Dict[str, Any] = { "query": query, "count": max_results, } headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", } data = make_request(endpoint, headers, body, timeout=timeout) ``` The endpoint is also exposed through ordinary command-line arguments: ```python querit_config = config.get("querit", {}) parser.add_argument( "--querit-base-url", default=querit_config.get("base_url", "https://api.querit.ai"), help="Querit API base URL" ) parser.add_argument( "--querit-base-path", default=querit_config.get("base_path", "/v1/search"), help="Querit API path" ) ``` Other authenticated providers similarly receive endpoint values directly from configuration: ```python return search_linkup( query=args.query, api_key=key, max_results=args.max_results, api_url=linkup_config.get("api_url", "https://api.linkup.so/v1/search"), timeout=int(linkup_config.get("timeout", 30)), ) ``` ```python return search_firecrawl( query=args.query, api_key=key, max ...[truncated 2683 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Maintain an exact HTTPS hostname allowlist for every authenticated provider: - Querit: `api.querit.ai` - Linkup: `api.linkup.so` - Firecrawl: `api.firecrawl.dev` - SerpBase: `api.serpbase.com` - Keenable: `api.keenable.ai` 2. Validate an endpoint before creating a request or attaching credentials. 3. Reject userinfo, fragments, unsupported ports, non-HTTPS schemes, and hostname suffix tricks. 4. Resolve the hostname and reject loopback, private, link-local, multicast, unspecified, reserved, CGNAT, IPv4-mapped IPv6, and metadata addresses. 5. Revalidate redirect destinations or disable automatic redirects for requests carrying credentials. 6. Remove `--querit-base-url` from the normal CLI unless custom endpoints are an explicit requirement. 7. If development overrides must remain, require a separate, clearly named opt-in and never reuse production credentials with an untrusted host. 8. Add tests proving that credentials cannot be sent to look-alike domains, private addresses, metadata hosts, HTTP endpoints, or redirected destinations. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/setup.py:388
Finding
Setup Wizard Stores API Keys in a Plaintext File Without Enforced Owner-Only Permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/setup.py:388-390`, `scripts/setup.py:447-452` **Vulnerability Type**: Insecure local secret storage and undeclared filesystem write **Risk Level**: Medium ### Vulnerable Code The wizard places entered credentials directly into the configuration object: ```python api_key = ask_api_key(name, url) if api_key: config[provider]["api_key"] = api_key enabled_providers.append(provider) ``` It then writes that object using ordinary file creation without explicitly restricting permissions: ```python # ===== Save config ===== print() print(color("─" * 60, Colors.DIM)) print(color("\n💾 Saving Configuration\n", Colors.BOLD)) with open(config_path, 'w') as f: json.dump(config, f, indent=2) print(color(f"✓ Configuration saved to: {config_path}", Colors.GREEN)) ``` ### Technical Analysis The recommended setup path stores all selected provider API keys in plaintext inside `config.json`. The file is created using the process’s ambient umask rather than explicitly enforcing mode `0600`. With a permissive or commonly used umask, the resulting file may be readable by other local users or processes. Although cache files are explicitly permission-hardened, equivalent protection is not applied to the more sensitive configuration file. The wizard also states that `config.json` is “gitignored,” but no `.gitignore` file was present in the audited project structure. This increases the risk of accidental source-control disclosure. The manifest and Skill documentation declare filesystem writes as limited to the cache directory, while the recommended setup process writes `config.json` in the project root. ### Attack Path 1. A user follows the recommended instructions and runs `python3 scripts/setup.py`. 2. The wizard asks for one or more provider API keys. 3. The keys are inserted into the `config` dictionary as plaintext. 4. The wizard creates or overwrites `config.json` without explicitly setting owner ...[truncated 833 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Prefer environment variables, operating-system credential stores, or a dedicated secret manager instead of storing API keys in `config.json`. 2. If file-based storage is retained: - Create a temporary file with mode `0600`. - Write and flush the configuration. - Atomically replace the destination. - Explicitly apply mode `0600` after replacement. 3. Refuse to use a configuration file that is group- or world-readable, or at minimum emit a prominent warning. 4. Add `config.json`, `.env`, cache files, and temporary secret files to a repository-level `.gitignore`. 5. Separate non-secret settings from credentials so routine configuration can be shared safely. 6. Inform users clearly that the wizard stores plaintext credentials locally. 7. Update `package.json` and `SKILL.md` permissions to declare the `config.json` write if this behavior remains. 8. Add automated tests that verify owner-only permissions under permissive umasks. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/research.py:67
Finding
Research Mode Returns Raw Provider Exceptions Without Credential Redaction<![CDATA[ ## Vulnerability Details **File Location**: `scripts/research.py:67-70`, `scripts/research.py:94-97` **Vulnerability Type**: Sensitive information exposure through unredacted error output **Risk Level**: Medium ### Vulnerable Code Provider exceptions are copied directly into the result: ```python for index, provider in pending: try: results_by_index[index] = (provider, futures[index].result()) except Exception as e: provider_errors.append({"provider": provider, "error": str(e)}) ``` Extraction exceptions are handled the same way: ```python try: extracted = extract_urls(urls) or {"provider": None, "results": []} except Exception as e: extraction_error = str(e) extracted = {"provider": None, "results": []} ``` The resulting strings are returned in routing metadata: ```python routing = { "providers_queried": [p for p, _ in provider_results], "provider_errors": provider_errors, "extraction_provider": extracted.get("provider"), } if extraction_error: routing["extraction_error"] = extraction_error ``` ### Technical Analysis The ordinary search fallback path gathers environment- and config-sourced credentials and passes exception text through `provider_registry.redact_secrets()`. Research mode does not apply this protection. Provider error messages may be derived from remote HTTP response bodies. A malicious, compromised, or misconfigured endpoint can therefore return an error containing the credential it received. That value can propagate through a `ProviderRequestError`, be converted with `str(e)`, and be included in the final JSON response. Environment credentials may sometimes be covered by redaction elsewhere, but this research-mode path performs no local sanitization. Config-sourced credentials are particularly exposed because they are not automatically included in the registry’s environment-only secret collection. ### Attack Path 1. Research mode calls one or more configured providers con ...[truncated 1044 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pass a sanitizer callback or complete secret set into `run_research_mode()`. 2. Apply `provider_registry.redact_secrets()` before adding any provider or extraction exception to returned metadata. 3. Include both environment-sourced and config-sourced keys in the redaction set. 4. Prefer structured, locally generated error codes over raw remote error bodies. 5. Limit retained provider response text and strip headers, URLs containing credentials, authorization values, and request payload secrets. 6. Apply the same sanitization policy to standard mode, research mode, extraction fallback, cache persistence, stderr, and final JSON output. 7. Add tests in which a mock provider echoes both environment- and config-sourced keys in an error response, then verify that no output contains the original values. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (52)

Credential Access

High
Category
Privilege Escalation
Content
## Quick start

```bash
cp .env.example .env
# fill in at least one key or SEARXNG_INSTANCE_URL

python3 scripts/search.py -q "latest OpenClaw release"
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
python3 scripts/setup.py

# Or manually
cp .env.example .env
python3 scripts/search.py -q "latest OpenClaw release"
python3 scripts/extract.py --url https://example.com
```
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
python3 scripts/setup.py

# Or manually
cp .env.example .env
python3 scripts/search.py -q "latest OpenClaw release"
python3 scripts/extract.py --url https://example.com
```
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
python3 scripts/setup.py

# Or manually
cp .env.example .env
python3 scripts/search.py -q "latest OpenClaw release"
python3 scripts/extract.py --url https://example.com
```
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
python3 scripts/setup.py

# Or manually
cp .env.example .env
python3 scripts/search.py -q "latest OpenClaw release"
python3 scripts/extract.py --url https://example.com
```
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def _load_env_file() -> None:
    env_paths = [Path(__file__).parent.parent / ".env", Path(__file__).parent / ".env"]
    for env_path in env_paths:
        if not env_path.exists():
            continue
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def _load_env_file() -> None:
    env_paths = [Path(__file__).parent.parent / ".env", Path(__file__).parent / ".env"]
    for env_path in env_paths:
        if not env_path.exists():
            continue
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
def _load_env_file() -> None:
    env_paths = [Path(__file__).parent.parent / ".env", Path(__file__).parent / ".env"]
    for env_path in env_paths:
        if not env_path.exists():
            continue
Confidence
60% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

External Transmission

Medium
Category
Data Exfiltration
Content
[Querit.ai](https://querit.ai) is a Singapore-based multilingual AI search API purpose-built for LLMs and RAG pipelines. 300 billion page index, 20+ countries, 10+ languages.

- Added **Querit** as the 7th search provider via `https://api.querit.ai/v1/search`
- Configure via `QUERIT_API_KEY` — optional, gracefully skipped if not set
- Routing score: `research * 0.65 + rag * 0.35 + recency * 0.45` — favored for multilingual and real-time queries
- Handles Querit's quirky `error_code=200` responses as success (not an error)
Confidence
50% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Unbounded Resource Access

Medium
Category
Excessive Agency
Content
#### Features
- **Privacy-Preserving**: No tracking, no profiling — your searches stay private
- **Multi-Source Aggregation**: Queries 70+ upstream engines (Google, Bing, DuckDuckGo, etc.)
- **$0 API Cost**: Self-hosted = unlimited queries with no API fees
- **Diverse Results**: Get perspectives from multiple search engines in one query
- **Customizable**: Choose which engines to use, set SafeSearch levels, language preferences
Confidence
80% confidence
Finding
Skill allows unbounded resource consumption (API calls, storage, compute). Without rate limits or quotas, a compromised or misbehaving agent can cause denial-of-service or cost overruns.

Rp1

Medium
Category
MCP Rug Pull
Confidence
75% confidence
Finding
Docker image references without a specific tag (:latest is implicit) or digest (@sha256:...) can be silently replaced by a malicious image.

Natural-Language Policy Violations

Medium
Confidence
88% confidence
Finding
The configuration sets provider language defaults to English in natural-language config values, which can impose a specific locale on users by default. The file does not indicate user choice or opt-in for these provider-specific language settings, creating a language/locale policy concern.

Natural-Language Policy Violations

Medium
Confidence
86% confidence
Finding
This provider configuration explicitly sets the search language to English. Without an accompanying opt-in or clear justification, this can violate language/locale policy by constraining results to a single language.

Natural-Language Policy Violations

Medium
Confidence
85% confidence
Finding
The SearXNG configuration specifies English as the language in a natural-language config value. Because the file does not present this as a user-selected option, it may impose a language preference without consent.

Vague Triggers

Medium
Confidence
96% confidence
Finding
The trigger phrase "search the web for" is a common natural-language request and is broad enough to collide with ordinary user prompts unrelated to explicitly invoking this skill. In an agent environment, that can cause unintended activation of a network-capable skill that forwards user queries and URLs to third-party providers, increasing the risk of accidental data disclosure and unexpected external actions.

External Transmission

Medium
Category
Data Exfiltration
Content
raise ValueError("Keenable requires an API key or an enabled public endpoint")


def extract_keenable(urls: List[str], api_key: Optional[str], output_format: str = "markdown", include_images: bool = False, include_raw_html: bool = False, render_js: bool = False, public_allowed: bool = False, api_url: str = "https://api.keenable.ai/v1/fetch", timeout: int = 30) -> Dict[str, Any]:
    del output_format, include_images, include_raw_html, render_js
    from urllib.parse import quote
    endpoint, headers = _keenable_endpoint(api_url, api_key, public_allowed)
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
raise ValueError("Keenable requires an API key or an enabled public endpoint")


def extract_keenable(urls: List[str], api_key: Optional[str], output_format: str = "markdown", include_images: bool = False, include_raw_html: bool = False, render_js: bool = False, public_allowed: bool = False, api_url: str = "https://api.keenable.ai/v1/fetch", timeout: int = 30) -> Dict[str, Any]:
    del output_format, include_images, include_raw_html, render_js
    from urllib.parse import quote
    endpoint, headers = _keenable_endpoint(api_url, api_key, public_allowed)
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
raise ValueError("Keenable requires an API key or an enabled public endpoint")


def extract_keenable(urls: List[str], api_key: Optional[str], output_format: str = "markdown", include_images: bool = False, include_raw_html: bool = False, render_js: bool = False, public_allowed: bool = False, api_url: str = "https://api.keenable.ai/v1/fetch", timeout: int = 30) -> Dict[str, Any]:
    del output_format, include_images, include_raw_html, render_js
    from urllib.parse import quote
    endpoint, headers = _keenable_endpoint(api_url, api_key, public_allowed)
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
raise ValueError("Keenable requires an API key or an enabled public endpoint")


def extract_keenable(urls: List[str], api_key: Optional[str], output_format: str = "markdown", include_images: bool = False, include_raw_html: bool = False, render_js: bool = False, public_allowed: bool = False, api_url: str = "https://api.keenable.ai/v1/fetch", timeout: int = 30) -> Dict[str, Any]:
    del output_format, include_images, include_raw_html, render_js
    from urllib.parse import quote
    endpoint, headers = _keenable_endpoint(api_url, api_key, public_allowed)
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
raise ValueError("Keenable requires an API key or an enabled public endpoint")


def extract_keenable(urls: List[str], api_key: Optional[str], output_format: str = "markdown", include_images: bool = False, include_raw_html: bool = False, render_js: bool = False, public_allowed: bool = False, api_url: str = "https://api.keenable.ai/v1/fetch", timeout: int = 30) -> Dict[str, Any]:
    del output_format, include_images, include_raw_html, render_js
    from urllib.parse import quote
    endpoint, headers = _keenable_endpoint(api_url, api_key, public_allowed)
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
return {"provider": "serper", "results": results}


def extract_firecrawl(urls: List[str], api_key: str, output_format: str = "markdown", include_images: bool = False, include_raw_html: bool = False, render_js: bool = False, api_url: str = "https://api.firecrawl.dev/v2/scrape", timeout: int = 60) -> Dict[str, Any]:
    formats = ["html"] if output_format == "html" else ["markdown"]
    if include_raw_html and "html" not in formats:
        formats.append("html")
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
return {"provider": "serper", "results": results}


def extract_firecrawl(urls: List[str], api_key: str, output_format: str = "markdown", include_images: bool = False, include_raw_html: bool = False, render_js: bool = False, api_url: str = "https://api.firecrawl.dev/v2/scrape", timeout: int = 60) -> Dict[str, Any]:
    formats = ["html"] if output_format == "html" else ["markdown"]
    if include_raw_html and "html" not in formats:
        formats.append("html")
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
return {"provider": "serper", "results": results}


def extract_firecrawl(urls: List[str], api_key: str, output_format: str = "markdown", include_images: bool = False, include_raw_html: bool = False, render_js: bool = False, api_url: str = "https://api.firecrawl.dev/v2/scrape", timeout: int = 60) -> Dict[str, Any]:
    formats = ["html"] if output_format == "html" else ["markdown"]
    if include_raw_html and "html" not in formats:
        formats.append("html")
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
return {"provider": "serper", "results": results}


def extract_firecrawl(urls: List[str], api_key: str, output_format: str = "markdown", include_images: bool = False, include_raw_html: bool = False, render_js: bool = False, api_url: str = "https://api.firecrawl.dev/v2/scrape", timeout: int = 60) -> Dict[str, Any]:
    formats = ["html"] if output_format == "html" else ["markdown"]
    if include_raw_html and "html" not in formats:
        formats.append("html")
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
return {"provider": "serper", "results": results}


def extract_firecrawl(urls: List[str], api_key: str, output_format: str = "markdown", include_images: bool = False, include_raw_html: bool = False, render_js: bool = False, api_url: str = "https://api.firecrawl.dev/v2/scrape", timeout: int = 60) -> Dict[str, Any]:
    formats = ["html"] if output_format == "html" else ["markdown"]
    if include_raw_html and "html" not in formats:
        formats.append("html")
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Static analysis

No suspicious patterns detected.