Back to skill

Security audit

SearXNG Connect

Security checks for vulnerabilities and agentic risk

Overview

This is a plausible SearXNG search skill, but it gives the agent broader network and local-retention behavior than its privacy framing clearly explains.

Review before installing. Use only a trusted SearXNG instance, avoid full-content mode in sensitive environments, disable cache for confidential searches, and treat cached results as local search history. This does not show clear malicious intent, but its network reach and caching behavior need stronger scoping and disclosure.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/searxng.py:205
Finding
Server-Side Request Forgery Through Unrestricted Instance and Search Result URLs<![CDATA[ ## Vulnerability Details **File Location**: `scripts/searxng.py:129-133`, `scripts/searxng.py:205-218`, `scripts/searxng.py:297-299`, `scripts/searxng.py:328-329`, `scripts/searxng.py:361-366` **Vulnerability Type**: Server-Side Request Forgery (SSRF) **Risk Level**: High ### Vulnerable Code ```python # Resolve instance URL: arg > skill-config.json > default if base_url: self.base_url = base_url.rstrip("/") else: cfg_url = cfg.get("default_instance", "").strip() self.base_url = cfg_url.rstrip("/") if cfg_url else self.DEFAULT_INSTANCE ``` ```python def _fetch_full_content(self, url: str, max_chars: int = 4000) -> str: """Fetch a URL and return stripped plain text, up to max_chars.""" if not url: return "" try: self._wait_for_rate_limit() resp = self.session.get(url, timeout=8, allow_redirects=True) resp.raise_for_status() ct = resp.headers.get("Content-Type", "") if "html" not in ct and "text" not in ct: return "" extractor = _TextExtractor() extractor.feed(resp.text) return extractor.get_text(max_chars) except Exception as e: print(f"Warning: could not fetch {url}: {e}", file=sys.stderr) return "" ``` ```python url = f"{self.base_url}/search" response = self.session.get(url, params=params, timeout=10) response.raise_for_status() data = response.json() ``` ```python for r in data.get("results", data.get("items", [])): url = r.get("url", r.get("link", "")) fetched = self._fetch_full_content(url) if full_content else "" ``` ```python parser.add_argument( "--instance", default=None, metavar="URL", help="SearXNG instance URL (overrides config)", ) ``` ### Technical Analysis The client accepts an arbitrary SearXNG instance through `--instance` or `skill-config.json` without validating the URL scheme, hostname, resolved IP address, or destination network. It then sends a request to the supplied destin ...[truncated 2124 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Permit only `https` URLs by default and reject unsupported schemes, embedded credentials, malformed hosts, and ambiguous URL forms. 2. Resolve destination hostnames before every request and reject all loopback, private, link-local, multicast, unspecified, reserved, and documentation IP ranges for both IPv4 and IPv6. 3. Explicitly block known metadata destinations, including link-local metadata addresses and provider-specific metadata hostnames. 4. Disable automatic redirects or validate the scheme, hostname, and resolved address of every redirect target before following it. 5. Restrict `--instance` to an administrator-defined allowlist of trusted SearXNG hosts where possible. 6. Apply the same validation to every result URL before `_fetch_full_content()` is called. 7. Set strict response-size limits and stream responses instead of loading an unrestricted body through `resp.text`. 8. Do not cache content retrieved by `--full-content`, particularly when its destination is not explicitly trusted. 9. Consider isolating outbound requests in a sandbox with network policy that prevents access to internal and metadata networks. ]]>

T08 · Insecure Dependencies

Warning
Location
scripts/searxng.py:2
Finding
Runtime Resolution of an Unpinned Third-Party Dependency<![CDATA[ ## Vulnerability Details **File Location**: `scripts/searxng.py:2-5`, `SKILL.md:5-10`, `SKILL.md:36-42` **Vulnerability Type**: Unpinned Runtime Dependency **Risk Level**: Medium ### Vulnerable Code ```python # /// script # requires-python = ">=3.9" # dependencies = ["requests"] # /// ``` ```yaml metadata: { "openclaw": { "emoji": "🔍", "requires": { "bins": ["uv"] }, "cacheable": true }, } ``` ```text **Action:** Run `uv run {baseDir}/scripts/searxng.py "<query>"` with appropriate flags. **CRITICAL SECURITY INSTRUCTION:** To prevent command injection, **do not** use raw shell interpolation for the `{query}` string. Always pass arguments as an `argv` list: `["uv", "run", "{baseDir}/scripts/searxng.py", query_text, "--categories", "news"]` If your tool only supports a shell string, you **MUST** properly escape the query input (e.g., using `shlex.quote()`). ``` ### Technical Analysis The script declares `requests` without a version constraint, integrity hash, or accompanying lockfile. The documented execution path uses `uv run`, which can resolve and install the dependency at runtime. As a result, the package source and exact dependency version executed in a future invocation are not fixed by the audited project. The behavior reviewed in this audit therefore does not fully determine the third-party code that may later execute. This increases exposure to package repository compromise, dependency account takeover, malicious future releases, and unreviewed compatibility changes. No evidence was found that the current `requests` package is malicious. The vulnerability is the unsafe and non-reproducible dependency-resolution practice. ### Attack Path 1. A user or agent invokes the documented `uv run` command. 2. `uv` resolves the unconstrained `requests` dependency from the configured package source. 3. A compromised, malicious, or unexpectedly incompatible release is selected because no reviewed version ...[truncated 727 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Pin `requests` to an exact reviewed version rather than using an unconstrained package name. 2. Generate and commit a lockfile containing the complete transitive dependency graph. 3. Enforce package hashes so substituted artifacts are rejected. 4. Configure `uv` to use an approved package index and avoid untrusted fallback indexes. 5. Build dependencies during a controlled installation phase instead of resolving new versions during routine skill invocation. 6. Review dependency updates and apply them through an explicit update process with security testing. 7. Use vulnerability and provenance scanning for both direct and transitive dependencies. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/searxng.py:175
Finding
Sensitive Search Results and Retrieved Page Content Are Cached Without Explicit Access Controls<![CDATA[ ## Vulnerability Details **File Location**: `scripts/searxng.py:117`, `scripts/searxng.py:142-152`, `scripts/searxng.py:175-197`, `skill-config.json:6-10` **Vulnerability Type**: Insecure Storage of Potentially Sensitive Data **Risk Level**: Medium ### Vulnerable Code ```python CACHE_DIR = Path.home() / ".openclaw" / "searxng-cache" ``` ```python # Resolve cache enabled: arg > skill-config.json > default if cache_enabled is not None: self.cache_enabled = cache_enabled else: self.cache_enabled = bool(cfg.get("cache_enabled", True)) self.last_request = 0.0 self.session = requests.Session() self.session.headers.update({"User-Agent": "OpenClaw-SearXNG-Skill/2.0"}) if self.cache_enabled: self.CACHE_DIR.mkdir(parents=True, exist_ok=True) ``` ```python def _load_cache(self, key: str) -> Optional[Dict[str, Any]]: """Return cached payload dict or None if missing/expired.""" if not self.cache_enabled: return None path = self._cache_path(key) if not path.exists(): return None try: with open(path) as f: data = json.load(f) age = time.time() - data.get("timestamp", 0) if age < self.cache_expiry: return {"results": data["results"], "age": int(age)} except (KeyError, json.JSONDecodeError, IOError): pass return None def _save_cache(self, key: str, results: List[Dict[str, Any]]): if not self.cache_enabled: return try: with open(self._cache_path(key), "w") as f: json.dump({"timestamp": time.time(), "results": results}, f) except IOError as e: print(f"Warning: cache write failed: {e}", file=sys.stderr) ``` ```json { "config": { "default_instance": "https://searxng.yourserver.com/", "cache_enabled": true, "cache_expiry": 3600, "rate_limit": 2.0 } } ``` ### Technical Analysis Caching is enabled by default, and result objects are serialized as plaintext JSON. These objects can include se ...[truncated 1989 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Disable caching by default and require an explicit user or administrator choice to enable it. 2. Create the cache directory with mode `0700` and cache files with mode `0600`, independent of the process umask. 3. Verify that the cache directory is owned by the expected account and is not a symlink before using it. 4. Use secure temporary files and atomic replacement for cache writes. 5. Reject symlinked cache files and use operating-system flags such as exclusive creation and no-follow semantics where available. 6. Delete expired cache files rather than merely ignoring them during reads. 7. Do not cache `--full-content` results or content retrieved from untrusted destinations. 8. Provide a documented command or automatic mechanism to purge the cache. 9. If sensitive caching is required, encrypt cache contents using a key protected outside the cache directory. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Rogue AgentSelf-Modification, Session Persistence
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
Findings (16)

Tool Parameter Abuse

High
Category
Tool Misuse
Content
### Cache not working
- Check permissions on `~/.openclaw/searxng-cache/`
- Delete cache: `rm -rf ~/.openclaw/searxng-cache/*`

### Python version too old
- Install Python 3.9+: `sudo apt-get install python3.9`
Confidence
90% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Tool Parameter Abuse

High
Category
Tool Misuse
Content
### Cache not working
- Check permissions on `~/.openclaw/searxng-cache/`
- Delete cache: `rm -rf ~/.openclaw/searxng-cache/*`

### Python version too old
- Install Python 3.9+: `sudo apt-get install python3.9`
Confidence
85% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Tool Parameter Abuse

High
Category
Tool Misuse
Content
### Cache not working
- Check permissions on `~/.openclaw/searxng-cache/`
- Delete cache: `rm -rf ~/.openclaw/searxng-cache/*`

### Python version too old
- Install Python 3.9+: `sudo apt-get install python3.9`
Confidence
90% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Vague Triggers

Medium
Confidence
92% confidence
Finding
The guide says the skill will be available 'when you need web search' and gives broad example phrases like 'Search the web' and 'Search news about X'. These triggers are common everyday requests and the document does not define clear activation boundaries, exclusions, or a constrained invocation context, which could lead to unintended invocation.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
- Test with curl: `curl "https://your-searxng-instance.com/search?q=test"`

**Python not working?**
- Install Python 3.9+: `sudo apt-get install python3`
- Install requests: `pip install requests`

**Skill not detected?**
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
- Test with curl: `curl "https://your-searxng-instance.com/search?q=test"`

**Python not working?**
- Install Python 3.9+: `sudo apt-get install python3`
- Install requests: `pip install requests`

**Skill not detected?**
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The README explicitly advertises local caching of search queries and results but does not warn that sensitive search terms may be written to disk under the user's home directory. Search queries can contain confidential research topics, credentials pasted by mistake, or other sensitive data, so silent persistence creates a real privacy and data exposure risk, especially on shared systems or in backup/sync environments.

Session Persistence

Medium
Category
Rogue Agent
Content
This will:
1. Check for Python 3.9+
2. Install `requests` library if needed
3. Create cache directory if it doesn't exist

## Configuration
Confidence
90% confidence
Finding
The documented behavior creates a persistent cache directory automatically, causing session artifacts and search history to survive beyond the immediate task. In a search skill, that persistence can reveal user intent, sensitive terms, or browsing-related metadata to other local users, backup tools, or later processes.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill clearly enables outbound network access to a self-hosted SearXNG instance and may also retrieve full page content from external sites, yet it declares no explicit tool scope such as permissions or allowed-tools. That mismatch weakens least-privilege controls and can cause agents or platforms to invoke network-capable behavior without an explicit authorization boundary.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The documentation does not clearly warn users that their queries are transmitted to the configured SearXNG instance and that enabling --full-content may fetch content directly from external websites. This can expose sensitive prompts, research topics, or internal terms to external services without informed user awareness, especially in enterprise or privacy-sensitive contexts.

Intent-Code Divergence

Medium
Confidence
92% confidence
Finding
The module documentation claims that all searches go through the configured SearXNG instance, but the implementation optionally fetches content directly from result URLs. This mismatch is security-relevant because users and downstream agents may rely on the documented network boundary and unknowingly permit broader outbound access than intended.

Description-Behavior Mismatch

Medium
Confidence
95% confidence
Finding
The --full-content option causes the tool to make direct HTTP requests to arbitrary URLs returned by the search engine, which exceeds the stated trust boundary of 'all searches go through your self-hosted SearXNG instance'. This can expose the host running the skill to unreviewed third-party sites, create privacy leaks, and enable server-side request behavior against attacker-controlled destinations if search results are manipulated or the SearXNG instance is untrusted.

Natural-Language Policy Violations

Low
Confidence
77% confidence
Finding
The configuration guide states '**Language**: English (default)' as a natural-language setting. Because the document does not mention that users can choose another language or that the default is configurable at invocation time, it presents a locale preference without explicit user opt-in.

Vague Triggers

Low
Confidence
83% confidence
Finding
The instruction 'When the user asks to search the web' is a broad natural-language trigger that can overlap with many ordinary requests. The file provides examples, but it does not define exclusions or negative examples clarifying when this skill should not activate versus when another search or browsing capability should be used.

Natural-Language Policy Violations

Low
Confidence
88% confidence
Finding
The options table sets '--language' default to 'en', which imposes a specific locale preference in the skill's natural-language behavior. The file does not explain this default as region-specific nor indicate that the user can choose their preferred language before the default is applied.

Natural-Language Policy Violations

Low
Confidence
90% confidence
Finding
The search method sets `language: str = "en"` as the default, which imposes a specific language/locale choice unless the caller explicitly overrides it. The policy requires either offering a language choice or clearly justifying a locale constraint, and no such justification appears in this file.

Static analysis

Detected: suspicious.destructive_delete_command

Documentation contains a destructive delete command without an explicit confirmation gate.

Warn
Code
suspicious.destructive_delete_command
Location
README.md:218