Back to skill

Security audit

Local Web Search

Security checks for vulnerabilities and agentic risk

Overview

This is a real web-search skill, but it needs review because it can fetch arbitrary URLs, use sensitive credentials, delegate browsing to a worker, and auto-install an unpinned package.

Install only if you are comfortable letting the skill make outbound web requests from your machine or container. Avoid using it on networks where localhost, private services, or cloud metadata endpoints are reachable unless egress is restricted. Do not source broad secrets.env files into shared agent sessions, and review or pin the Gemini dependency before using the Gemini wrapper.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/browse_page.py:230
Finding

Unrestricted URL Fetching Enables Server-Side Request Forgery

Content
View full analysis
tuple: """Returns (status_code, raw_html_str).""" ua = ( "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/122.0.0.0 Safari/537.36" ) req = urllib.request.Request(url, headers={ "User-Agent": ua, "Accept": "text/html,application/xhtml+xml,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.9", "Referer": "https://www.google.com/", }) with urllib.request.urlopen(req, timeout=timeout) as r: ct = r.headers.get("Content-Type", "utf-8") ``` From `scripts/browse_page.py:248-264`: ```python def _browser_worker_fetch(url: str, mode: str, timeout: int) -> dict: payload = { "url": url, "mode": mode, "extract": "text", "timeout_ms": timeout * 1000, } req = urllib.request.Request( f"{BROWSER_WORKER_URL.rstrip('/')}/render", data=json.dumps(payload).encode("utf-8"), headers={"Content-Type": "application/json"}, ) request_timeout = max(timeout + 10, timeout * 3) if mode in ("auto", "stealth", "dynamic"): request_timeout = max(request_timeout, 75) with urllib.request.urlopen(req, timeout=request_timeout) as resp: body = json.loads(resp.read().decode("utf-8")) ``` From `scripts/browse_page.py:430-489`: ```python if BROWSER_WORKER_URL and mode in ("auto", "stealth", "dynamic"): try: return _browser_worker_fetch(url, mode, timeout) except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, j ...[truncated 7988 chars]
Remediation
View remediation

T08 · Insecure Dependencies

Warning
Location
requirements.txt:1
Finding

Automatic Installation of Unpinned Dependencies Creates a Supply-Chain Execution Risk

Content
View full analysis
=1.0.0 ``` Complete contents of `scripts/run_gemini_search.sh`: ```bash #!/usr/bin/env bash set -euo pipefail DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PY="$DIR/.venv/bin/python" if [[ ! -x "$PY" ]]; then python3 -m venv "$DIR/.venv" "$DIR/.venv/bin/python" -m pip install -r "$DIR/requirements.txt" fi exec "$PY" "$DIR/scripts/gemini_google_search.py" "$@" ``` ### Technical Analysis The requirement uses an open-ended lower bound rather than a reviewed exact version. On the first Gemini search invocation, the wrapper automatically creates a virtual environment and installs the newest package version accepted by `google-genai>=1.0.0`, together with its current transitive dependency graph. No lock file, upper version constraint, or package hash verification is present. The code installed and subsequently imported can therefore change after the Skill has been reviewed. Installation also uses the runtime pip configuration and package index without verifying that they are trusted. This is not evidence that the current `google-genai` package is malicious. The vulnerability is that normal Skill execution implicitly retrieves and executes mutable third-party code without reproducible integrity controls. ### Attack Path 1. The `.venv` directory does not exist or does not contain an executable Python interpreter. 2. A user or Agent invokes `run_gemini_search.sh`. 3. The wrapper automatically runs pip against the configured package index. 4. Pip resolves the newest compatible `google-genai` release and its transitive dependencies. 5. If the index, package account, dependency graph, DNS/proxy path, or future compatible release is compromised, attack ...[truncated 954 chars]
Remediation
View remediation
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • 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
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (24)

Tainted flow: 'req' from os.environ.get (line 255, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/browse_page.py (reported line 239)May include surrounding context.

python
"Accept-Language": "en-US,en;q=0.9",
        "Referer": "https://www.google.com/",
    })
    with urllib.request.urlopen(req, timeout=timeout) as r:
        ct = r.headers.get("Content-Type", "utf-8")
        charset = "utf-8"
        m = re.search(r"charset=([\w-]+)", ct)

Tainted flow: 'req' from os.environ.get (line 255, credential/environment) → urllib.request.urlopen (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

If BROWSER_WORKER_URL is set from the environment, the script will POST the target URL and fetch instructions to that external service. That contradicts the local-only expectation of the skill and can leak browsing targets and retrieved content metadata to a remote worker under environment control, which is significant in a search/browsing skill that may process sensitive research queries.

Content

Scanner excerpt · scripts/browse_page.py (reported line 263)May include surrounding context.

python
request_timeout = max(timeout + 10, timeout * 3)
    if mode in ("auto", "stealth", "dynamic"):
        request_timeout = max(request_timeout, 75)
    with urllib.request.urlopen(req, timeout=request_timeout) as resp:
        body = json.loads(resp.read().decode("utf-8"))
    content = body.get("text") or ""
    return {

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding

The description claims a comprehensive real-time web search skill with search engine selection, aggregation, validation, and optional Google-grounded synthesis. The actual code chunk only implements the browse/view portion: retrieving a single user-supplied URL using a fetcher cascade (fast/stealth/dynamic or browser-worker), extracting readable content, and assigning a simple confidence score. Some declared details are partially reflected, such as proxy detection and the three-tier browsing modes, but the core primary purpose is materially different: search versus single-page fetch. Therefore this code chunk does not accurately represent the declared end-to-end skill and should be flagged as a mismatch.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding

The code does implement one declared sub-feature: optional Gemini API Google Search grounding that produces synthesized answers with citations. However, the declared description presents the skill primarily as a broad real-time web search system with local/private SearXNG default behavior and many additional search, browsing, and validation capabilities. This code chunk does not implement any of those broader/default mechanisms; it is only a standalone Gemini grounding helper. It also accesses 1Password CLI to retrieve API keys, which is not mentioned in the declared permissions or description, though that is ancillary credential handling rather than the primary mismatch. Overall, the actual behavior is a small optional component of the declared system, not an accurate representation of the full declared capability, so this is a description-behavior mismatch.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
90% confidence
Finding

The code largely matches the core declared purpose of local/private web search using SearXNG with parallel multi-engine search, intent-aware expansion, proxy detection, and optional fallback. However, the description materially overstates several capabilities not present in this code chunk. There is no Gemini API integration or Google-grounded answer synthesis. The described three-tier browsing stack is not implemented here; the script merely shells out to browse_page.py for the top result when --browse is used. Likewise, 'multi-source factual claim cross-verification' is stronger than the actual behavior, which is limited to URL/result deduplication and counting overlap across engines rather than verifying factual claims from page contents. These are meaningful description-versus-behavior gaps, so this should be flagged as a mismatch.

Content

No source excerpt is available for this finding.

Credential Access

High
Category
Privilege Escalation
Confidence
89% confidence
Finding

The documentation instructs operators to source a local secrets file to enable 1Password-backed credential access. In agent-driven environments, encouraging shell-level loading of secrets into the process environment expands exposure of sensitive credentials to any downstream subprocesses, logs, debugging output, or prompt-influenced command misuse.

Content

Scanner excerpt · SKILL.md (reported line 204)May include surrounding context.

--op-vault OpenClaw-Core --op-item openclaw-gemini-api

text

If the workspace has `secrets.env`, source it first for non-interactive 1Password service-account access. Never print secret values.

```bash
set -a; source ./secrets.env; set +a

Credential Access

High
Category
Privilege Escalation
Confidence
89% confidence
Finding

The example operational flow normalizes environment-based secret loading for non-interactive access, which can unintentionally expose API keys or service-account material to other tools in the same session. Because this skill also uses shell execution and external network calls, the combination raises the impact of accidental disclosure or exfiltration if the environment is inspected or reused.

Content

Scanner excerpt · SKILL.md (reported line 207)May include surrounding context.

If the workspace has secrets.env, source it first for non-interactive 1Password service-account access. Never print secret values.

bash
set -a; source ./secrets.env; set +a
skills/local-web-search/scripts/run_gemini_search.sh \
  --query "latest Home Assistant release" \
  --op-vault OpenClaw-Core \

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding

The skill declares no explicit tool scope or permission boundaries even though the documented workflow clearly relies on environment access, file reads, network access, and shell execution. In an agent environment, this increases the chance that a model can invoke broader capabilities than users or operators expect, which weakens least-privilege controls and makes misuse harder to audit.

Content

No source excerpt is available for this finding.

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The security manifest claims only direct HTTP GETs to the provided URL, but the implementation can instead send a POST request to a browser-worker endpoint and delegate retrieval there. This is dangerous because operators and downstream agents may make trust decisions based on inaccurate documentation, causing unintended disclosure of browsing activity to third-party infrastructure.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The request headers hard-code "Accept-Language: en-US,en;q=0.9", which imposes a specific language/locale on fetched content regardless of user preference. This is a natural-language policy concern because the skill does not offer any language choice or document a justified locale restriction.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/gemini_google_search.py (reported line 57)May include surrounding context.

python
if field:
        ref = f"op://{vault}/{item}/{field}"
        try:
            return subprocess.check_output(["op", "read", ref], text=True, stderr=subprocess.PIPE).strip()
        except (subprocess.CalledProcessError, FileNotFoundError) as exc:
            raise RuntimeError(f"Could not read 1Password field {ref}: {exc}") from exc

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/gemini_google_search.py (reported line 62)May include surrounding context.

python
raise RuntimeError(f"Could not read 1Password field {ref}: {exc}") from exc

    try:
        raw = subprocess.check_output(
            ["op", "item", "get", item, "--vault", vault, "--format", "json"],
            text=True,
            stderr=subprocess.PIPE,

Context-Inappropriate Capability

Medium
Category
Not specified by scanner
Confidence
83% confidence
Finding

For a web-search skill, making HTTP requests is expected, but this implementation also executes external programs: curl for search fallback and browse_page.py via a child Python process. Process execution is a broader capability than necessary for the stated search purpose unless explicitly justified in the manifest or skill scope.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
97% confidence
Finding

The module documentation states that "stdlib urllib" is used as a fallback, but _fetch_single_stdlib explicitly invokes subprocess.run with the external curl binary instead of using urllib. This is an active contradiction in the file's own documentation and changes the operational model from pure Python HTTP to spawning a process.

Content

No source excerpt is available for this finding.

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/search_local_web.py (reported line 335)May include surrounding context.

python
]

    try:
        proc = subprocess.run(cmd, capture_output=True, check=False, text=True, timeout=20)
        if proc.returncode != 0:
            return []
        data = json.loads(proc.stdout)

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/search_local_web.py (reported line 556)May include surrounding context.

python
print()
            browse_script = Path(__file__).parent / "browse_page.py"
            try:
                subprocess.run(
                    [sys.executable, str(browse_script), "--url", top_url, "--max-words", "600"],
                    check=False,
                )

subprocess module call

Medium
Category
Dangerous Code Execution
Confidence
70% confidence
Finding

subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Content

Scanner excerpt · scripts/verify_claim.py (reported line 229)May include surrounding context.

python
url,
    ]
    try:
        proc = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout + 2)
        if proc.returncode != 0:
            return None
        return proc.stdout[:1024 * 200]

Context-Inappropriate Capability

Low
Category
Not specified by scanner
Confidence
85% confidence
Finding

The manifest frames this skill as providing local web search, browsing, and factual verification. However, the compatibility section states it is designed for any LLM that can run shell commands via OpenClaw's tool interface, exposing a general command-execution dependency that is not itself a search capability and is broader than what the high-level purpose suggests.

Content

No source excerpt is available for this finding.

Unpinned Dependencies

Low
Category
Supply Chain
Confidence
95% confidence
Finding

The dependency is specified with only a lower bound (google-genai>=1.0.0), which allows future major or minor releases to be installed without review. This creates supply-chain and reliability risk because a breaking or compromised upstream release could be pulled automatically during installation, especially in a network-facing search skill that may handle external content and optional third-party API access.

Content

Scanner excerpt · requirements.txt (reported line 1)May include surrounding context.

text
google-genai>=1.0.0

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
85% confidence
Finding

The manifest says no credentials are sent externally, but proxy URLs loaded from environment variables can contain embedded usernames and passwords and are used for outbound requests. Even if this is common proxy behavior, the documentation is materially inaccurate and can lead users to underestimate where secrets may be presented during network access.

Content

No source excerpt is available for this finding.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · scripts/gemini_google_search.py (reported line 22)May include surrounding context.

python
return default
    if isinstance(obj, dict):
        return obj.get(name, default)
    return getattr(obj, name, default)


def _to_plain(obj: Any) -> Any:

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · scripts/gemini_google_search.py (reported line 35)May include surrounding context.

python
if isinstance(obj, dict):
        return {str(k): _to_plain(v) for k, v in obj.items()}
    for meth in ("to_json_dict", "model_dump", "dict"):
        fn = getattr(obj, meth, None)
        if callable(fn):
            try:
                return _to_plain(fn())

Context-Inappropriate Capability

Low
Category
Not specified by scanner
Confidence
86% confidence
Finding

The skill's stated purpose is web search with optional Gemini-backed grounding, but this helper also implements secret retrieval from local environment variables and a 1Password CLI subprocess flow. While credential use is understandable for an optional API-backed mode, embedding secret-manager access and subprocess execution is a distinct capability not clearly justified by the search helper's core purpose as described for this file.

Content

No source excerpt is available for this finding.

Natural-Language Policy Violations

Low
Category
Not specified by scanner
Confidence
95% confidence
Finding

The SearXNG query hard-codes language to en, which imposes a specific locale on all searches. This is a natural-language policy concern because the skill does not offer user opt-in or explain why English-only behavior is required.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.