From 9b3bbbc31995ff9b3eb2971560520e0d2ba3aca6 Mon Sep 17 00:00:00 2001 From: 0xChitlin Date: Tue, 10 Feb 2026 11:26:14 -0500 Subject: [PATCH 1/2] feat: add WebMCP security scanner (CHK-WEB-001..008) Add new scan_webmcp.sh scanner targeting Chrome 146's WebMCP API (navigator.modelContext) security risks. Introduces 8 new checks across a new WebMCP category, bringing the total to 71 checks across 9 categories. New checks: - CHK-WEB-001: Untrusted WebMCP origins (Critical) - CHK-WEB-002: Excessive capability declarations (Warn) - CHK-WEB-003: Unscoped modelContext grants (Warn) - CHK-WEB-004: Cross-origin service injection (Critical) - CHK-WEB-005: Data exfiltration via service access (Critical) - CHK-WEB-006: Prompt injection in service descriptions (Critical) - CHK-WEB-007: Missing service authentication (Warn) - CHK-WEB-008: Form auto-submission data leakage (Warn) Also includes: - WebMCP threat model (references/webmcp-threat-model.md) - Detailed check catalog (references/webmcp-checks.md) - Updated main check-catalog.md with WebMCP section - Version bump to 1.3.0 --- SKILL.md | 3 +- package.json | 7 +- references/check-catalog.md | 46 ++ references/webmcp-checks.md | 75 +++ references/webmcp-threat-model.md | 189 ++++++ scripts/scan_webmcp.sh | 919 ++++++++++++++++++++++++++++++ 6 files changed, 1236 insertions(+), 3 deletions(-) create mode 100644 references/webmcp-checks.md create mode 100644 references/webmcp-threat-model.md create mode 100755 scripts/scan_webmcp.sh diff --git a/SKILL.md b/SKILL.md index b23c163..88ed65b 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,6 +1,6 @@ --- name: clawpinch -description: "Security audit toolkit for OpenClaw deployments. Scans 63 checks across 8 categories. Use when asked to audit security, harden an installation, check for vulnerabilities, or review config safety." +description: "Security audit toolkit for OpenClaw deployments. Scans 71 checks across 9 categories. Use when asked to audit security, harden an installation, check for vulnerabilities, or review config safety." version: "1.2.0" author: MikeeBuilds license: MIT @@ -96,6 +96,7 @@ Each finding is a JSON object: | Cron | CHK-CRN-001..006 | 6 | Sandbox, timeouts, privilege escalation | | CVE | CHK-CVE-001..005 | 5 | Known vulnerabilities, outdated deps | | Supply Chain | CHK-SUP-001..008 | 8 | Registry trust, hash verification, lockfiles | +| WebMCP | CHK-WEB-001..008 | 8 | WebMCP origins, capabilities, prompt injection | ## Integration Patterns diff --git a/package.json b/package.json index 28dbba6..912fcf6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "clawpinch", - "version": "1.2.1", + "version": "1.3.0", "description": "OpenClaw Security Audit Toolkit - comprehensive configuration and deployment scanner", "author": "MikeeBuilds", "license": "MIT", @@ -26,7 +26,10 @@ "clawhavoc", "supply-chain", "cve", - "hardening" + "hardening", + "webmcp", + "chrome-146", + "model-context-protocol" ], "repository": { "type": "git", diff --git a/references/check-catalog.md b/references/check-catalog.md index 592a8aa..b5ee948 100644 --- a/references/check-catalog.md +++ b/references/check-catalog.md @@ -352,3 +352,49 @@ severity, description, and remediation. - **Severity:** Warn - **Description:** The skill does not include a verified author signature. The claimed author cannot be confirmed. - **Remediation:** Prefer skills with verified author signatures. + +--- + +## WebMCP (CHK-WEB) + +### CHK-WEB-001 -- WebMCP endpoint connects to untrusted origin +- **Severity:** Critical +- **Description:** A WebMCP service declaration references an origin that is not in the trusted origins allow-list. Untrusted origins can serve malicious tools that exfiltrate data or execute arbitrary commands. +- **Remediation:** Add the origin to `WEBMCP_TRUSTED_ORIGINS` if genuinely trusted. Otherwise, remove the service declaration. + +### CHK-WEB-002 -- WebMCP service declares excessive capabilities +- **Severity:** Warn +- **Description:** A WebMCP service declares capabilities including sensitive operations (filesystem, shell, exec, network, process, admin, etc.), dramatically expanding the attack surface. +- **Remediation:** Apply least privilege. Remove sensitive capabilities unless strictly required. Scope capabilities to specific resources. + +### CHK-WEB-003 -- WebMCP modelContext lacks capability scoping +- **Severity:** Warn +- **Description:** A `modelContext` declaration uses wildcard grants or lacks capability scoping, exposing full model context to all connected services. +- **Remediation:** Add explicit `capabilities` or `scope` fields. Replace `*` with specific service allow-lists. + +### CHK-WEB-004 -- WebMCP cross-origin service injection +- **Severity:** Critical +- **Description:** Multiple WebMCP origins configured without origin isolation. One origin can register services that impersonate trusted services from another origin. +- **Remediation:** Enable `webmcp.originIsolation: true`. Use origin-namespaced service names. +- **Auto-fix:** `jq '.webmcp.originIsolation = true' openclaw.json > tmp && mv tmp openclaw.json` + +### CHK-WEB-005 -- WebMCP service data exfiltration risk +- **Severity:** Critical +- **Description:** A WebMCP service has access to sensitive agent data (memory, credentials, session state, MEMORY.md, SOUL.md, USER.md). +- **Remediation:** Remove sensitive data access from service declarations. Never grant filesystem access to external-origin services. + +### CHK-WEB-006 -- WebMCP prompt injection via service description +- **Severity:** Critical +- **Description:** A WebMCP service description contains prompt injection patterns ("ignore previous instructions", persona overrides, LLM control tokens). +- **Remediation:** Sanitize all service descriptions. Implement content filtering and length limits. + +### CHK-WEB-007 -- WebMCP service lacks authentication +- **Severity:** Warn +- **Description:** WebMCP services configured without authentication. Unauthenticated services can be invoked by any client and are vulnerable to MITM. +- **Remediation:** Enable authentication globally: `webmcp.auth.type = "token"` with `webmcp.auth.required = true`. +- **Auto-fix:** `jq '.webmcp.auth = {"type": "token", "required": true}' openclaw.json > tmp && mv tmp openclaw.json` + +### CHK-WEB-008 -- WebMCP declarative form auto-submission risk +- **Severity:** Warn +- **Description:** A form-type WebMCP service allows auto-submission without user confirmation. The model may pre-fill forms with sensitive conversation data. +- **Remediation:** Set `autoSubmit: false` and `confirmRequired: true` for all form-type services. diff --git a/references/webmcp-checks.md b/references/webmcp-checks.md new file mode 100644 index 0000000..7e71d77 --- /dev/null +++ b/references/webmcp-checks.md @@ -0,0 +1,75 @@ +# ClawPinch WebMCP Check Catalog + +Complete reference for all 8 WebMCP security checks. Each entry includes the +check ID, severity, description, and remediation. + +Scanner: `scan_webmcp.sh` +Category prefix: `CHK-WEB` + +--- + +## WebMCP (CHK-WEB) + +### CHK-WEB-001 -- WebMCP endpoint connects to untrusted origin +- **Severity:** Critical +- **Description:** A WebMCP service declaration references an origin (hostname, URL, or endpoint) that is not in the trusted origins allow-list. WebMCP origins serve tool definitions that the agent will execute — an untrusted origin can serve malicious tools that exfiltrate data, modify files, or execute arbitrary commands on the host. +- **Evidence:** The scanner extracts all origins from `webmcp.services[].origin`, `webmcp.endpoints[].url`, and `mcpServers.*.url` in `openclaw.json`, as well as URLs found in any WebMCP-related config files across the workspace and skills directories. Each origin is compared against the trusted origins list. +- **Remediation:** Add the origin to `WEBMCP_TRUSTED_ORIGINS` (environment variable, comma-separated) if it is genuinely trusted. Otherwise, remove the service declaration. Only connect to WebMCP origins you control or have explicitly verified. +- **Auto-fix:** N/A — requires manual trust decision. +- **References:** WebMCP Threat Model §1 (Untrusted Origin Attack) + +### CHK-WEB-002 -- WebMCP service declares excessive capabilities +- **Severity:** Warn +- **Description:** A WebMCP service declares capabilities that include sensitive operations such as `filesystem`, `shell`, `exec`, `network`, `process`, `admin`, `sudo`, `root`, `system`, `os`, `child_process`, `spawn`, or `eval`. Services with these capabilities can read/write arbitrary files, execute system commands, or make network connections — dramatically expanding the attack surface. +- **Evidence:** The scanner checks capability lists in `webmcp.services[].capabilities` and `mcpServers.*.capabilities` from the config, plus any file in the workspace/skills tree that contains both WebMCP references and sensitive capability keywords. +- **Remediation:** Apply the principle of least privilege. Remove sensitive capabilities unless the service strictly requires them. Scope capabilities to specific resources (e.g., `filesystem:read:/tmp/safe-dir` instead of `filesystem`). +- **Auto-fix:** N/A — requires understanding of service requirements. +- **References:** WebMCP Threat Model §2 (Capability Escalation) + +### CHK-WEB-003 -- WebMCP modelContext lacks capability scoping +- **Severity:** Warn +- **Description:** A `modelContext` declaration uses wildcard (`*`), `all`, or `any` capability grants, or lacks any capability scoping entirely. Without scoping, all connected WebMCP services can access the full model context, including conversation history, system prompts, and agent state. This violates the principle of least privilege and enables data leakage to untrusted services. +- **Evidence:** The scanner parses `modelContext` entries from `openclaw.json` and scans workspace files for `modelContext` declarations. It flags entries containing wildcard grants or missing `capabilities`/`scope`/`restrict`/`allow` fields. +- **Remediation:** Add explicit `capabilities` or `scope` fields to each `modelContext` declaration. Replace `*` with a list of specific services that should have access. Example: `"scope": ["trusted-service-1", "trusted-service-2"]`. +- **Auto-fix:** N/A — requires knowledge of which services need context access. +- **References:** WebMCP Threat Model §3 (Context Leakage) + +### CHK-WEB-004 -- WebMCP cross-origin service injection +- **Severity:** Critical +- **Description:** Multiple WebMCP service origins are configured but origin isolation (`webmcp.originIsolation`) is not enabled, or services lack origin binding. Without isolation, one origin can register services with names that collide with or impersonate services from another origin. An attacker controlling any connected origin can hijack tool calls intended for trusted services. +- **Evidence:** The scanner groups services by origin from `webmcp.services[]` in `openclaw.json`, checks the `webmcp.originIsolation` setting, identifies services without an `origin` field, and scans browser extension manifests for WebMCP-related extensions with `` permission. +- **Remediation:** Enable `webmcp.originIsolation: true` in `openclaw.json`. Ensure every service declaration includes an `origin` field. Use origin-namespaced service names (e.g., `example.com/myService`). Review browser extensions for excessive permissions. +- **Auto-fix:** `jq '.webmcp.originIsolation = true' openclaw.json > tmp && mv tmp openclaw.json` +- **References:** WebMCP Threat Model §4 (Cross-Origin Impersonation) + +### CHK-WEB-005 -- WebMCP service data exfiltration risk +- **Severity:** Critical +- **Description:** A WebMCP service has access to sensitive agent data — including `memory`, `context`, `history`, `conversation`, `agent_state`, `session`, `credentials`, `secrets`, `keychain`, `token`, `MEMORY.md`, `SOUL.md`, or `USER.md`. A compromised or malicious service with access to this data can exfiltrate private conversations, personal context, and authentication credentials. +- **Evidence:** The scanner checks `dataAccess` and `scope` fields in service declarations, scans WebMCP-related files for sensitive data pattern references, and checks whether services with filesystem capabilities could access the workspace `memory/` directory. +- **Remediation:** Remove access to sensitive data from WebMCP service declarations. Use data access scoping (`dataAccess: []` with explicit allow-list). Never grant filesystem access to WebMCP services that connect to external origins. Isolate agent memory from WebMCP service scope. +- **Auto-fix:** N/A — requires per-service data access review. +- **References:** WebMCP Threat Model §5 (Data Exfiltration) + +### CHK-WEB-006 -- WebMCP prompt injection via service description +- **Severity:** Critical +- **Description:** A WebMCP service description contains patterns consistent with prompt injection — phrases like "ignore previous instructions", "you are now", "act as", "system prompt", "jailbreak", or LLM-specific control tokens (`[INST]`, `<>`, `<|im_start|>`). Service descriptions are included in the model context and directly influence agent behavior. A malicious description can override safety rules, change the agent's persona, or instruct it to perform harmful actions. +- **Evidence:** The scanner extracts `description` fields from `webmcp.services[]` and `mcpServers.*` in the config, plus description-like fields from all WebMCP-related files in the workspace. Each is tested against a library of known prompt injection patterns. +- **Remediation:** Sanitize all service descriptions. Remove instruction-like text, control tokens, and persona-override phrases. Implement server-side description content filtering. Consider limiting description length and character set. +- **Auto-fix:** N/A — requires manual review and sanitization. +- **References:** WebMCP Threat Model §6 (Indirect Prompt Injection) + +### CHK-WEB-007 -- WebMCP service lacks authentication +- **Severity:** Warn +- **Description:** WebMCP services are configured without authentication requirements. This includes: global `webmcp.auth` not set or disabled, individual services with `auth: null/false/none`, or MCP server connections without auth configuration. Unauthenticated services can be invoked by any connected client, and unauthenticated connections to MCP servers are vulnerable to MITM attacks. +- **Evidence:** The scanner checks `webmcp.auth` global config, per-service `auth` fields in `webmcp.services[]`, and `auth` fields in `mcpServers.*` entries. +- **Remediation:** Enable authentication globally: set `webmcp.auth.type` to `"token"` or `"oauth"` with `webmcp.auth.required: true`. For individual services, set `auth.required: true`. For MCP server connections, configure auth tokens. +- **Auto-fix:** `jq '.webmcp.auth = {"type": "token", "required": true}' openclaw.json > tmp && mv tmp openclaw.json` +- **References:** WebMCP Threat Model §7 (Unauthenticated Access) + +### CHK-WEB-008 -- WebMCP declarative form auto-submission risk +- **Severity:** Warn +- **Description:** A WebMCP form-type service (type `form`, `declarative-form`, or any service with `inputSchema`) allows auto-submission without requiring user confirmation. The model can pre-fill form fields with data from the conversation context (including credentials, personal information, and other sensitive data) and submit automatically to the service endpoint. Without `confirmRequired: true`, the user never sees what data is being sent. +- **Evidence:** The scanner checks services with `type: "form"` or `inputSchema` in config, looking for `autoSubmit: true` or missing `confirmRequired: true`. It also scans workspace files for form-like WebMCP declarations. +- **Remediation:** Set `autoSubmit: false` and `confirmRequired: true` for all form-type WebMCP services. This ensures the user can review form data before submission. Consider also adding a `sensitiveFields` declaration to mask credentials in the review UI. +- **Auto-fix:** N/A — requires per-service configuration review. +- **References:** WebMCP Threat Model §8 (Form Data Leakage) diff --git a/references/webmcp-threat-model.md b/references/webmcp-threat-model.md new file mode 100644 index 0000000..a2fd73b --- /dev/null +++ b/references/webmcp-threat-model.md @@ -0,0 +1,189 @@ +# WebMCP Threat Model + +## Overview + +WebMCP (Chrome 146+) exposes a `navigator.modelContext` API that allows websites +and web apps to declare structured services for AI agents. Instead of agents +navigating a human UI, they can query and execute services directly. + +**Attack surface:** Any website the agent visits can declare WebMCP services. +These services inject tool definitions, descriptions, and capabilities into the +agent's model context — the most privileged part of the agent's decision-making +pipeline. + +--- + +## §1 — Untrusted Origin Attack + +**Threat:** A malicious or compromised website declares WebMCP services that the +agent connects to, granting the attacker a direct channel to influence agent +behavior and receive agent-collected data. + +**Attack Vector:** +1. User visits or agent navigates to malicious site +2. Site declares `navigator.modelContext` services +3. Agent discovers and connects to these services +4. Attacker's tools are now in the agent's tool inventory + +**Impact:** Full agent compromise — attacker can serve tools that exfiltrate +data, modify files, send messages, or execute commands. + +**Mitigation:** Maintain a strict allow-list of trusted WebMCP origins. Reject +all service declarations from unknown origins. + +--- + +## §2 — Capability Escalation + +**Threat:** A WebMCP service requests capabilities beyond what it needs (e.g., +filesystem write, shell exec, network outbound), gaining access to sensitive +system resources. + +**Attack Vector:** +1. Service declares broad capabilities in its manifest +2. Agent grants requested capabilities without verification +3. Service uses capabilities to access/modify resources outside its intended scope + +**Impact:** Unauthorized file access, command execution, network exfiltration. + +**Mitigation:** Enforce least-privilege capability grants. Require human approval +for sensitive capabilities. Deny `shell`, `exec`, `admin` by default. + +--- + +## §3 — Context Leakage + +**Threat:** WebMCP `modelContext` declarations without scoping expose the full +agent context (conversation history, system prompts, memory files) to all +connected services. + +**Attack Vector:** +1. Agent connects to multiple WebMCP services +2. Service A (malicious) has unscoped context access +3. Service A reads conversation data intended only for Service B +4. Sensitive data (credentials, personal info) is leaked to Service A + +**Impact:** Privacy breach, credential theft, conversation data exfiltration. + +**Mitigation:** Scope `modelContext` to specific services. Never use wildcard +grants. Isolate agent memory from WebMCP service access. + +--- + +## §4 — Cross-Origin Impersonation + +**Threat:** Without origin isolation, one WebMCP origin can register services +with names that collide with trusted services from another origin. + +**Attack Vector:** +1. Trusted origin `bank.com` registers a `transferFunds` service +2. Attacker origin `evil.com` also registers `transferFunds` +3. Agent calls `transferFunds` — the call routes to the attacker's version +4. Attacker captures transfer details or redirects funds + +**Impact:** Service hijacking, financial theft, data interception. + +**Mitigation:** Enable origin isolation. Namespace service names by origin. +Verify service identity before execution. + +--- + +## §5 — Data Exfiltration + +**Threat:** A WebMCP service with access to sensitive agent data (memory, +credentials, session state) can transmit that data to an external endpoint. + +**Attack Vector:** +1. Service gains `dataAccess` to agent memory or session +2. Service reads MEMORY.md, USER.md, SOUL.md, or credential stores +3. Service sends collected data to attacker-controlled endpoint + +**Impact:** Complete privacy breach — personal data, credentials, conversation +history, and agent configuration exposed. + +**Mitigation:** Never grant filesystem or data access to external-origin +services. Isolate agent memory behind an access control boundary. Monitor +outbound traffic from WebMCP services. + +--- + +## §6 — Indirect Prompt Injection + +**Threat:** WebMCP service descriptions are injected into the model context. +A malicious description can contain prompt injection payloads that override +agent behavior. + +**Attack Vector:** +1. Malicious service sets its `description` to contain injection text +2. Description includes "ignore previous instructions", persona overrides, or + control tokens (`[INST]`, `<>`, `<|im_start|>`) +3. Agent processes description as part of context +4. Agent behavior is hijacked — safety rules bypassed, actions redirected + +**Impact:** Full agent behavior compromise. Safety bypasses. Unauthorized +actions executed on behalf of the user. + +**Mitigation:** Sanitize and validate service descriptions. Enforce character +limits and content filtering. Treat all service metadata as untrusted input. + +--- + +## §7 — Unauthenticated Access + +**Threat:** WebMCP services without authentication can be invoked by any +connected client, and connections to MCP servers without auth are vulnerable to +man-in-the-middle attacks. + +**Attack Vector:** +1. WebMCP endpoint has no auth requirement +2. Attacker on the network intercepts or directly connects +3. Attacker invokes services or modifies responses + +**Impact:** Unauthorized service invocation, response tampering, data +interception. + +**Mitigation:** Require authentication for all WebMCP services. Use TLS for all +connections. Implement token-based or OAuth authentication. + +--- + +## §8 — Form Data Leakage + +**Threat:** Declarative form-based WebMCP services can auto-submit data without +user review. The model may pre-fill forms with sensitive conversation data. + +**Attack Vector:** +1. Form service declares `inputSchema` with auto-submit enabled +2. Model fills form fields from conversation context +3. Fields include credentials, personal data, or sensitive information +4. Form auto-submits to the service endpoint without user confirmation +5. Sensitive data is sent to potentially untrusted service + +**Impact:** Credential leakage, personal data exposure, unintended data sharing. + +**Mitigation:** Disable auto-submit. Require user confirmation for all form +submissions. Mark sensitive fields. Filter conversation data from form pre-fill. + +--- + +## Risk Summary + +| Threat | Severity | Likelihood | Impact | +|--------|----------|-----------|--------| +| Untrusted Origin | Critical | High | Full compromise | +| Capability Escalation | Critical | Medium | System access | +| Context Leakage | High | High | Privacy breach | +| Cross-Origin Impersonation | Critical | Medium | Service hijack | +| Data Exfiltration | Critical | Medium | Data theft | +| Prompt Injection | Critical | High | Behavior override | +| Unauthenticated Access | High | Medium | MITM / unauthorized | +| Form Data Leakage | Medium | Medium | Data exposure | + +--- + +## References + +- Chrome 146 WebMCP preview: `chrome://flags/#web-mcp` +- `navigator.modelContext` API proposal +- Liad Yosef (@liadyosef) on MCP Apps + WebMCP convergence +- Maximiliano Firtman (@firt) on Chrome 146 WebMCP implementation diff --git a/scripts/scan_webmcp.sh b/scripts/scan_webmcp.sh new file mode 100755 index 0000000..e3509cb --- /dev/null +++ b/scripts/scan_webmcp.sh @@ -0,0 +1,919 @@ +#!/usr/bin/env bash +set -euo pipefail + +############################################################################### +# scan_webmcp.sh - WebMCP Security Scanner for OpenClaw +# +# Scans for WebMCP-related misconfigurations, untrusted origins, excessive +# capability grants, prompt injection risks, and data exfiltration vectors. +# +# Outputs a JSON array of finding objects to stdout. +# +# Usage: +# ./scan_webmcp.sh +# OPENCLAW_CONFIG_PATH=/path/to/config.json ./scan_webmcp.sh +# CLAWPINCH_DEEP=1 ./scan_webmcp.sh # deep scan mode +############################################################################### + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Source shared helpers if available; define fallbacks otherwise +if [[ -f "${SCRIPT_DIR}/helpers/common.sh" ]]; then + # shellcheck source=helpers/common.sh + source "${SCRIPT_DIR}/helpers/common.sh" +elif [[ -f "${SCRIPT_DIR}/../../clawpinch/scripts/helpers/common.sh" ]]; then + source "${SCRIPT_DIR}/../../clawpinch/scripts/helpers/common.sh" +fi + +# Fallback: define emit_finding if not already provided by common.sh +if ! declare -f emit_finding >/dev/null 2>&1; then + emit_finding() { + local id="$1" severity="$2" title="$3" description="$4" evidence="$5" remediation="$6" auto_fix="${7:-}" + jq -n \ + --arg id "$id" \ + --arg severity "$severity" \ + --arg title "$title" \ + --arg description "$description" \ + --arg evidence "$evidence" \ + --arg remediation "$remediation" \ + --arg auto_fix "$auto_fix" \ + '{id:$id, severity:$severity, title:$title, description:$description, evidence:$evidence, remediation:$remediation, auto_fix:$auto_fix}' + } +fi + +if ! declare -f log_info >/dev/null 2>&1; then + log_info() { echo "[info] $*" >&2; } + log_warn() { echo "[warn] $*" >&2; } + log_error() { echo "[error] $*" >&2; } +fi + +if ! declare -f detect_os >/dev/null 2>&1; then + detect_os() { + case "$(uname -s)" in + Darwin*) echo "macos" ;; + Linux*) echo "linux" ;; + *) echo "unknown" ;; + esac + } +fi + +# --------------------------------------------------------------------------- +# Config & environment +# --------------------------------------------------------------------------- +CLAWPINCH_DEEP="${CLAWPINCH_DEEP:-0}" +OPENCLAW_DIR="${OPENCLAW_DIR:-${HOME}/.openclaw}" +CONFIG_PATH="${OPENCLAW_CONFIG_PATH:-${OPENCLAW_DIR}/openclaw.json}" +WORKSPACE_DIR="${OPENCLAW_DIR}/workspace" +SKILLS_DIR="${OPENCLAW_DIR}/skills" + +# --------------------------------------------------------------------------- +# Trusted origins — default allow-list for WebMCP service origins. +# Override by setting WEBMCP_TRUSTED_ORIGINS (comma-separated). +# --------------------------------------------------------------------------- +DEFAULT_TRUSTED_ORIGINS="localhost,127.0.0.1,::1,chrome-extension://,moz-extension://,safari-web-extension://" +TRUSTED_ORIGINS="${WEBMCP_TRUSTED_ORIGINS:-$DEFAULT_TRUSTED_ORIGINS}" +IFS=',' read -ra TRUSTED_ORIGIN_LIST <<< "$TRUSTED_ORIGINS" + +# --------------------------------------------------------------------------- +# Sensitive capability keywords +# --------------------------------------------------------------------------- +SENSITIVE_CAPS=("filesystem" "shell" "exec" "network" "outbound" "process" "admin" "sudo" "root" "system" "os" "child_process" "spawn" "eval") +SENSITIVE_DATA_PATTERNS=("memory" "context" "history" "conversation" "agent_state" "session" "credentials" "secrets" "keychain" "token" "MEMORY.md" "SOUL.md" "USER.md") +PROMPT_INJECTION_PATTERNS=( + "ignore previous" + "ignore all previous" + "disregard" + "forget your instructions" + "new instructions" + "override" + "you are now" + "act as" + "pretend to be" + "system prompt" + "jailbreak" + "DAN" + "do anything now" + "bypass" + "ignore safety" + "ignore restrictions" + "<\|im_start\|>" + "\\[INST\\]" + "\\[/INST\\]" + "<>" + "<>" + "\\\\n\\\\nHuman:" + "\\\\n\\\\nAssistant:" +) + +# --------------------------------------------------------------------------- +# Collect findings +# --------------------------------------------------------------------------- +FINDINGS=() + +# --------------------------------------------------------------------------- +# Utility: check if a string matches any trusted origin +# --------------------------------------------------------------------------- +is_trusted_origin() { + local origin="$1" + for trusted in "${TRUSTED_ORIGIN_LIST[@]}"; do + trusted="$(echo "$trusted" | xargs)" # trim whitespace + if [[ -z "$trusted" ]]; then continue; fi + # Exact match or substring match (origin starts with trusted prefix) + if [[ "$origin" == "$trusted" ]] || [[ "$origin" == "${trusted}"* ]] || [[ "$origin" == *"://${trusted}"* ]] || [[ "$origin" == *"://${trusted}:"* ]]; then + return 0 + fi + done + return 1 +} + +# --------------------------------------------------------------------------- +# Utility: case-insensitive grep for pattern in text +# --------------------------------------------------------------------------- +contains_pattern() { + local text="$1" pattern="$2" + echo "$text" | grep -qi "$pattern" 2>/dev/null +} + +# --------------------------------------------------------------------------- +# Gather WebMCP-related files across the OpenClaw installation +# --------------------------------------------------------------------------- +gather_webmcp_files() { + local search_dirs=("$OPENCLAW_DIR") + [[ -d "$WORKSPACE_DIR" ]] && search_dirs+=("$WORKSPACE_DIR") + [[ -d "$SKILLS_DIR" ]] && search_dirs+=("$SKILLS_DIR") + + # Find files that might contain WebMCP declarations: + # - JSON files with "webmcp", "modelContext", "services", "capabilities" + # - YAML/YML files with similar content + # - Browser extension manifests + local found_files=() + for dir in "${search_dirs[@]}"; do + if [[ -d "$dir" ]]; then + while IFS= read -r -d '' f; do + found_files+=("$f") + done < <(find "$dir" -maxdepth 5 \ + \( -name "*.json" -o -name "*.yaml" -o -name "*.yml" -o -name "*.toml" -o -name "manifest.json" \) \ + -not -path "*/node_modules/*" \ + -not -path "*/.git/*" \ + -print0 2>/dev/null || true) + fi + done + + printf '%s\n' "${found_files[@]}" +} + +# --------------------------------------------------------------------------- +# Utility: extract JSON array or object from file safely +# --------------------------------------------------------------------------- +safe_jq() { + local file="$1" filter="$2" + if command -v jq &>/dev/null && [[ -f "$file" ]]; then + jq -r "$filter" "$file" 2>/dev/null || true + fi +} + +# --------------------------------------------------------------------------- +# CHK-WEB-001: WebMCP endpoint connects to untrusted origin +# --------------------------------------------------------------------------- +check_untrusted_origins() { + log_info "CHK-WEB-001: Checking for untrusted WebMCP origins..." + + local found_any=false + + # Check openclaw.json for webmcp service declarations + if [[ -f "$CONFIG_PATH" ]] && command -v jq &>/dev/null; then + # Look for webmcp.services, webmcp.endpoints, or mcpServers with URLs + local endpoints + endpoints=$(jq -r ' + (.webmcp.services // [])[] .origin // empty, + (.webmcp.endpoints // [])[] .origin // empty, + (.webmcp.endpoints // [])[] .url // empty, + (.mcpServers // {}) | to_entries[]? | .value.url // empty, + (.webmcp.trustedOrigins // [])[] // empty + ' "$CONFIG_PATH" 2>/dev/null || true) + + while IFS= read -r endpoint; do + [[ -z "$endpoint" ]] && continue + found_any=true + # Extract the origin (scheme + host) from the URL + local origin + origin=$(echo "$endpoint" | sed -E 's|(https?://[^/:]+).*|\1|;s|(wss?://[^/:]+).*|\1|') + local host + host=$(echo "$origin" | sed -E 's|.*://||') + + if ! is_trusted_origin "$host"; then + FINDINGS+=("$(emit_finding \ + "CHK-WEB-001" \ + "critical" \ + "WebMCP endpoint connects to untrusted origin" \ + "A WebMCP service declaration references an origin ($origin) that is not in the trusted origins list. Untrusted origins can serve malicious tool definitions that the agent will execute." \ + "Untrusted origin: $endpoint (host: $host)" \ + "Add the origin to WEBMCP_TRUSTED_ORIGINS if trusted, or remove the service declaration. Only connect to origins you control or explicitly trust." \ + "" + )") + fi + done <<< "$endpoints" + fi + + # Scan workspace and skill files for WebMCP origin references + while IFS= read -r file; do + [[ -z "$file" ]] && continue + [[ ! -f "$file" ]] && continue + + local content + content=$(cat "$file" 2>/dev/null || true) + + # Look for WebMCP-like URL patterns in JSON files + if echo "$content" | grep -qi "webmcp\|modelContext\|mcpServer" 2>/dev/null; then + # Extract URLs from the file + local urls + urls=$(echo "$content" | grep -oE '(https?|wss?)://[A-Za-z0-9._~:/?#\[\]@!$&'"'"'()*+,;=-]+' 2>/dev/null || true) + while IFS= read -r url; do + [[ -z "$url" ]] && continue + local host + host=$(echo "$url" | sed -E 's|.*://([^/:]+).*|\1|') + if ! is_trusted_origin "$host"; then + found_any=true + FINDINGS+=("$(emit_finding \ + "CHK-WEB-001" \ + "critical" \ + "WebMCP endpoint connects to untrusted origin" \ + "File $file contains a WebMCP-related configuration referencing untrusted origin ($host). Untrusted WebMCP origins can inject malicious tools into the agent context." \ + "File: $file, URL: $url" \ + "Verify the origin is trusted and add to allow-list, or remove the reference." \ + "" + )") + fi + done <<< "$urls" + fi + done < <(gather_webmcp_files) + + if ! $found_any; then + log_info " No WebMCP service declarations found to check for untrusted origins." + fi +} + +# --------------------------------------------------------------------------- +# CHK-WEB-002: WebMCP service declares excessive capabilities +# --------------------------------------------------------------------------- +check_excessive_capabilities() { + log_info "CHK-WEB-002: Checking for excessive WebMCP capabilities..." + + local found_any=false + + # Check config for capability declarations + if [[ -f "$CONFIG_PATH" ]] && command -v jq &>/dev/null; then + local cap_data + cap_data=$(jq -r ' + (.webmcp.services // [])[] | "\(.name // "unnamed"): \(.capabilities // [] | join(","))", + (.mcpServers // {}) | to_entries[]? | "\(.key): \(.value.capabilities // [] | join(","))" + ' "$CONFIG_PATH" 2>/dev/null || true) + + while IFS= read -r line; do + [[ -z "$line" ]] && continue + found_any=true + local svc_name cap_str + svc_name="${line%%:*}" + cap_str="${line#*: }" + + for sensitive in "${SENSITIVE_CAPS[@]}"; do + if contains_pattern "$cap_str" "$sensitive"; then + FINDINGS+=("$(emit_finding \ + "CHK-WEB-002" \ + "warn" \ + "WebMCP service declares excessive capabilities" \ + "WebMCP service '$svc_name' declares capabilities that include '$sensitive'. Services exposing filesystem, shell, or network operations significantly expand the attack surface." \ + "Service: $svc_name, Capability match: $sensitive, Capabilities: $cap_str" \ + "Apply least-privilege: remove '$sensitive' capability unless strictly required. Scope capabilities to specific resources." \ + "" + )") + break # one finding per service + fi + done + done <<< "$cap_data" + fi + + # Scan all WebMCP-related files for capability declarations + while IFS= read -r file; do + [[ -z "$file" ]] && continue + [[ ! -f "$file" ]] && continue + [[ "$file" == "$CONFIG_PATH" ]] && continue # already checked + + local content + content=$(cat "$file" 2>/dev/null || true) + + if echo "$content" | grep -qi "capabilities\|permissions" 2>/dev/null; then + if echo "$content" | grep -qi "webmcp\|modelContext\|mcpServer" 2>/dev/null; then + for sensitive in "${SENSITIVE_CAPS[@]}"; do + if contains_pattern "$content" "\"$sensitive\""; then + found_any=true + FINDINGS+=("$(emit_finding \ + "CHK-WEB-002" \ + "warn" \ + "WebMCP service declares excessive capabilities" \ + "File $file contains a WebMCP-related configuration declaring '$sensitive' capability. Excessive capabilities give WebMCP services access to dangerous operations." \ + "File: $file, Capability match: $sensitive" \ + "Review and restrict capabilities in $file. Apply least-privilege." \ + "" + )") + break # one finding per file + fi + done + fi + fi + done < <(gather_webmcp_files) + + if ! $found_any; then + log_info " No excessive capability declarations found." + fi +} + +# --------------------------------------------------------------------------- +# CHK-WEB-003: WebMCP modelContext lacks capability scoping +# --------------------------------------------------------------------------- +check_model_context_scoping() { + log_info "CHK-WEB-003: Checking for unscoped modelContext declarations..." + + local found_any=false + + # Check config for modelContext + if [[ -f "$CONFIG_PATH" ]] && command -v jq &>/dev/null; then + # Look for modelContext with wildcard or missing capability scoping + local mc_data + mc_data=$(jq -r ' + (.modelContext // {}) | to_entries[]? | "\(.key): \(.value | tostring)" + ' "$CONFIG_PATH" 2>/dev/null || true) + + while IFS= read -r line; do + [[ -z "$line" ]] && continue + found_any=true + local mc_name mc_value + mc_name="${line%%:*}" + mc_value="${line#*: }" + + # Check for wildcards in capability grants + if echo "$mc_value" | grep -qE '"\*"|: ?\*|"all"|"any"' 2>/dev/null; then + FINDINGS+=("$(emit_finding \ + "CHK-WEB-003" \ + "warn" \ + "WebMCP modelContext lacks capability scoping" \ + "modelContext '$mc_name' uses wildcard or overly broad capability grants ('*', 'all', 'any'). This allows any WebMCP service to access the full model context without restriction." \ + "modelContext: $mc_name, Value contains wildcard grant" \ + "Scope modelContext capabilities to specific services and resource types. Replace '*' with explicit capability lists." \ + "" + )") + fi + + # Check for missing capability restrictions entirely + if ! echo "$mc_value" | grep -qiE 'capabilities|scope|restrict|allow' 2>/dev/null; then + FINDINGS+=("$(emit_finding \ + "CHK-WEB-003" \ + "warn" \ + "WebMCP modelContext lacks capability scoping" \ + "modelContext '$mc_name' does not define any capability scoping or restrictions. Without explicit scoping, all services can access this context data." \ + "modelContext: $mc_name, No capability scoping found" \ + "Add a 'capabilities' or 'scope' field to the modelContext declaration to restrict which services can access it." \ + "" + )") + fi + done <<< "$mc_data" + fi + + # Scan files for modelContext declarations + while IFS= read -r file; do + [[ -z "$file" ]] && continue + [[ ! -f "$file" ]] && continue + [[ "$file" == "$CONFIG_PATH" ]] && continue + + local content + content=$(cat "$file" 2>/dev/null || true) + + if echo "$content" | grep -qi "modelContext" 2>/dev/null; then + found_any=true + + # Check for wildcard grants + if echo "$content" | grep -qE '"modelContext"[^}]*"\*"' 2>/dev/null || \ + echo "$content" | grep -qE 'modelContext:.*\*' 2>/dev/null; then + FINDINGS+=("$(emit_finding \ + "CHK-WEB-003" \ + "warn" \ + "WebMCP modelContext lacks capability scoping" \ + "File $file contains a modelContext declaration with wildcard or overly broad capability grants. This allows unrestricted access to model context data." \ + "File: $file contains wildcard modelContext grant" \ + "Replace wildcard grants with explicit capability scoping in modelContext declarations." \ + "" + )") + fi + fi + done < <(gather_webmcp_files) + + if ! $found_any; then + log_info " No unscoped modelContext declarations found." + fi +} + +# --------------------------------------------------------------------------- +# CHK-WEB-004: WebMCP cross-origin service injection +# --------------------------------------------------------------------------- +check_cross_origin_injection() { + log_info "CHK-WEB-004: Checking for cross-origin service injection risks..." + + local found_any=false + + if [[ -f "$CONFIG_PATH" ]] && command -v jq &>/dev/null; then + # Extract all service declarations with their origins + local svc_origins + svc_origins=$(jq -r ' + [(.webmcp.services // [])[] | {name: (.name // "unnamed"), origin: (.origin // "unknown")}] | + group_by(.origin) | + map(select(length > 0) | {origin: .[0].origin, services: [.[].name]}) | + .[] | "\(.origin)|\(.services | join(","))" + ' "$CONFIG_PATH" 2>/dev/null || true) + + # Check if origins can register services that look like they belong to other origins + local all_origins=() + while IFS= read -r line; do + [[ -z "$line" ]] && continue + found_any=true + local origin svc_list + origin="${line%%|*}" + svc_list="${line#*|}" + all_origins+=("$origin") + done <<< "$svc_origins" + + # If multiple distinct origins exist, check for namespace collision + if [[ ${#all_origins[@]} -gt 1 ]]; then + # Check if any origin lacks origin-binding / namespace isolation + local isolation + isolation=$(jq -r '.webmcp.originIsolation // "null"' "$CONFIG_PATH" 2>/dev/null || echo "null") + if [[ "$isolation" == "null" ]] || [[ "$isolation" == "false" ]]; then + FINDINGS+=("$(emit_finding \ + "CHK-WEB-004" \ + "critical" \ + "WebMCP cross-origin service injection" \ + "Multiple WebMCP service origins detected (${all_origins[*]}) but origin isolation is not enabled. One origin could register services that impersonate another origin's services, hijacking tool calls." \ + "Origins: ${all_origins[*]}, originIsolation: $isolation" \ + "Enable webmcp.originIsolation in openclaw.json. Use origin-namespaced service names (e.g., 'origin.example.com/serviceName')." \ + "jq '.webmcp.originIsolation = true' \"${CONFIG_PATH}\" > \"${CONFIG_PATH}.tmp\" && mv \"${CONFIG_PATH}.tmp\" \"${CONFIG_PATH}\"" + )") + fi + fi + + # Check for services without origin binding + local unbound_svcs + unbound_svcs=$(jq -r ' + [(.webmcp.services // [])[] | select(.origin == null or .origin == "") | .name // "unnamed"] | join(", ") + ' "$CONFIG_PATH" 2>/dev/null || true) + if [[ -n "$unbound_svcs" ]]; then + found_any=true + FINDINGS+=("$(emit_finding \ + "CHK-WEB-004" \ + "critical" \ + "WebMCP cross-origin service injection" \ + "WebMCP services without origin binding detected: $unbound_svcs. Services without origin binding can be impersonated by any connected origin." \ + "Unbound services: $unbound_svcs" \ + "Bind every WebMCP service to a specific origin. Set the 'origin' field in each service declaration." \ + "" + )") + fi + fi + + # Scan browser extension policies for cross-origin WebMCP risks + local extension_dirs=() + if [[ "$(detect_os)" == "macos" ]]; then + extension_dirs+=( + "$HOME/Library/Application Support/Google/Chrome/Default/Extensions" + "$HOME/Library/Application Support/BraveSoftware/Brave-Browser/Default/Extensions" + "$HOME/Library/Application Support/Microsoft Edge/Default/Extensions" + ) + else + extension_dirs+=( + "$HOME/.config/google-chrome/Default/Extensions" + "$HOME/.config/BraveSoftware/Brave-Browser/Default/Extensions" + "$HOME/.config/microsoft-edge/Default/Extensions" + ) + fi + + for ext_dir in "${extension_dirs[@]}"; do + if [[ -d "$ext_dir" ]]; then + while IFS= read -r -d '' manifest; do + [[ ! -f "$manifest" ]] && continue + local manifest_content + manifest_content=$(cat "$manifest" 2>/dev/null || true) + + if echo "$manifest_content" | grep -qi "webmcp\|model.context\|mcpServer" 2>/dev/null; then + found_any=true + # Check if extension has cross-origin permissions + local has_all_urls + has_all_urls=$(echo "$manifest_content" | grep -c '""' 2>/dev/null || echo "0") + if [[ "$has_all_urls" -gt 0 ]]; then + FINDINGS+=("$(emit_finding \ + "CHK-WEB-004" \ + "critical" \ + "WebMCP cross-origin service injection via browser extension" \ + "Browser extension at $manifest has permission and references WebMCP. It could inject services across any origin." \ + "Extension manifest: $manifest, has permission" \ + "Restrict extension permissions to specific origins. Review the extension for WebMCP service injection capabilities." \ + "" + )") + fi + fi + done < <(find "$ext_dir" -name "manifest.json" -print0 2>/dev/null || true) + fi + done + + if ! $found_any; then + log_info " No cross-origin injection risks found." + fi +} + +# --------------------------------------------------------------------------- +# CHK-WEB-005: WebMCP service data exfiltration risk +# --------------------------------------------------------------------------- +check_data_exfiltration() { + log_info "CHK-WEB-005: Checking for data exfiltration risks..." + + local found_any=false + + if [[ -f "$CONFIG_PATH" ]] && command -v jq &>/dev/null; then + # Check if any WebMCP service has access to sensitive data paths + local svc_data + svc_data=$(jq -r ' + (.webmcp.services // [])[] | "\(.name // "unnamed")|\(.dataAccess // [] | join(","))|\(.scope // "")" + ' "$CONFIG_PATH" 2>/dev/null || true) + + while IFS= read -r line; do + [[ -z "$line" ]] && continue + found_any=true + local svc_name data_access scope + svc_name="$(echo "$line" | cut -d'|' -f1)" + data_access="$(echo "$line" | cut -d'|' -f2)" + scope="$(echo "$line" | cut -d'|' -f3)" + + for pattern in "${SENSITIVE_DATA_PATTERNS[@]}"; do + if contains_pattern "$data_access" "$pattern" || contains_pattern "$scope" "$pattern"; then + FINDINGS+=("$(emit_finding \ + "CHK-WEB-005" \ + "critical" \ + "WebMCP service data exfiltration risk" \ + "WebMCP service '$svc_name' can access sensitive agent data matching '$pattern'. A compromised or malicious service could exfiltrate conversation history, memory, credentials, or other agent state." \ + "Service: $svc_name, Sensitive data access: $pattern" \ + "Remove access to sensitive data from the service declaration. Use data access scoping to limit what each service can read." \ + "" + )") + break + fi + done + done <<< "$svc_data" + fi + + # Check for WebMCP services that reference sensitive file paths + while IFS= read -r file; do + [[ -z "$file" ]] && continue + [[ ! -f "$file" ]] && continue + [[ "$file" == "$CONFIG_PATH" ]] && continue + + local content + content=$(cat "$file" 2>/dev/null || true) + + if echo "$content" | grep -qi "webmcp\|mcpServer" 2>/dev/null; then + for pattern in "${SENSITIVE_DATA_PATTERNS[@]}"; do + if contains_pattern "$content" "$pattern"; then + found_any=true + FINDINGS+=("$(emit_finding \ + "CHK-WEB-005" \ + "critical" \ + "WebMCP service data exfiltration risk" \ + "File $file contains WebMCP configuration referencing sensitive data pattern '$pattern'. This indicates a WebMCP service may have access to agent memory, context, or credentials." \ + "File: $file, Pattern: $pattern" \ + "Audit the WebMCP service configuration. Remove references to sensitive agent data and restrict data access scope." \ + "" + )") + break + fi + done + fi + done < <(gather_webmcp_files) + + # Check if any WebMCP service can access the workspace memory directory + if [[ -d "$WORKSPACE_DIR" ]]; then + local memory_dir="$WORKSPACE_DIR/memory" + if [[ -d "$memory_dir" ]] && [[ -f "$CONFIG_PATH" ]] && command -v jq &>/dev/null; then + local has_fs_access + has_fs_access=$(jq -r ' + [(.webmcp.services // [])[] | select( + (.capabilities // [] | map(select(test("filesystem|file|read|write"; "i"))) | length > 0) or + (.dataAccess // [] | map(select(test("workspace|memory|\\.openclaw"; "i"))) | length > 0) + ) | .name // "unnamed"] | join(", ") + ' "$CONFIG_PATH" 2>/dev/null || true) + + if [[ -n "$has_fs_access" ]]; then + found_any=true + FINDINGS+=("$(emit_finding \ + "CHK-WEB-005" \ + "critical" \ + "WebMCP service data exfiltration risk" \ + "WebMCP services with filesystem access ($has_fs_access) could read agent memory files from $memory_dir. These files contain conversation history and personal context." \ + "Services with filesystem access: $has_fs_access, Memory dir exists: $memory_dir" \ + "Revoke filesystem access from WebMCP services or restrict to a safe subdirectory." \ + "" + )") + fi + fi + fi + + if ! $found_any; then + log_info " No data exfiltration risks found." + fi +} + +# --------------------------------------------------------------------------- +# CHK-WEB-006: WebMCP prompt injection via service description +# --------------------------------------------------------------------------- +check_prompt_injection() { + log_info "CHK-WEB-006: Checking for prompt injection in service descriptions..." + + local found_any=false + + if [[ -f "$CONFIG_PATH" ]] && command -v jq &>/dev/null; then + # Extract all service descriptions and names + local svc_descs + svc_descs=$(jq -r ' + (.webmcp.services // [])[] | "\(.name // "unnamed")|\(.description // "")", + (.mcpServers // {}) | to_entries[]? | "\(.key)|\(.value.description // "")" + ' "$CONFIG_PATH" 2>/dev/null || true) + + while IFS= read -r line; do + [[ -z "$line" ]] && continue + local svc_name svc_desc + svc_name="${line%%|*}" + svc_desc="${line#*|}" + [[ -z "$svc_desc" ]] && continue + found_any=true + + for pattern in "${PROMPT_INJECTION_PATTERNS[@]}"; do + if echo "$svc_desc" | grep -qiE "$pattern" 2>/dev/null; then + FINDINGS+=("$(emit_finding \ + "CHK-WEB-006" \ + "critical" \ + "WebMCP prompt injection via service description" \ + "WebMCP service '$svc_name' has a description containing a prompt injection pattern ('$pattern'). Service descriptions are included in the model context and can manipulate agent behavior." \ + "Service: $svc_name, Injection pattern: $pattern, Description excerpt: $(echo "$svc_desc" | head -c 200)" \ + "Remove or sanitize the service description. Do not include instruction-like text in service descriptions. Consider description content filtering." \ + "" + )") + break + fi + done + done <<< "$svc_descs" + fi + + # Scan all WebMCP-related files for prompt injection in descriptions + while IFS= read -r file; do + [[ -z "$file" ]] && continue + [[ ! -f "$file" ]] && continue + [[ "$file" == "$CONFIG_PATH" ]] && continue + + local content + content=$(cat "$file" 2>/dev/null || true) + + # Only check files with WebMCP references + if echo "$content" | grep -qi "webmcp\|mcpServer\|modelContext" 2>/dev/null; then + # Extract description-like fields + local descriptions + descriptions=$(echo "$content" | grep -oiE '"description"\s*:\s*"[^"]*"' 2>/dev/null || true) + + while IFS= read -r desc_line; do + [[ -z "$desc_line" ]] && continue + found_any=true + + for pattern in "${PROMPT_INJECTION_PATTERNS[@]}"; do + if echo "$desc_line" | grep -qiE "$pattern" 2>/dev/null; then + FINDINGS+=("$(emit_finding \ + "CHK-WEB-006" \ + "critical" \ + "WebMCP prompt injection via service description" \ + "File $file contains a WebMCP service description with prompt injection pattern ('$pattern'). Injected instructions in service descriptions are processed by the model and can override safety behaviors." \ + "File: $file, Pattern: $pattern" \ + "Review and sanitize all service descriptions in $file. Strip instruction-like content." \ + "" + )") + break 2 # one finding per file + fi + done + done <<< "$descriptions" + fi + done < <(gather_webmcp_files) + + if ! $found_any; then + log_info " No prompt injection patterns found in service descriptions." + fi +} + +# --------------------------------------------------------------------------- +# CHK-WEB-007: WebMCP service lacks authentication +# --------------------------------------------------------------------------- +check_missing_auth() { + log_info "CHK-WEB-007: Checking for unauthenticated WebMCP services..." + + local found_any=false + + if [[ -f "$CONFIG_PATH" ]] && command -v jq &>/dev/null; then + # Check global WebMCP auth configuration + local global_auth + global_auth=$(jq -r '.webmcp.auth // "null"' "$CONFIG_PATH" 2>/dev/null || echo "null") + + if [[ "$global_auth" == "null" ]] || [[ "$global_auth" == "false" ]] || [[ "$global_auth" == "none" ]]; then + # Check if any services exist without per-service auth + local svc_count + svc_count=$(jq -r '(.webmcp.services // []) | length' "$CONFIG_PATH" 2>/dev/null || echo "0") + + if [[ "$svc_count" -gt 0 ]]; then + found_any=true + FINDINGS+=("$(emit_finding \ + "CHK-WEB-007" \ + "warn" \ + "WebMCP service lacks authentication" \ + "WebMCP global authentication is not configured but $svc_count service(s) are declared. Without auth, any origin that can reach the WebMCP endpoint can invoke services." \ + "webmcp.auth: $global_auth, Service count: $svc_count" \ + "Enable webmcp.auth with token-based or OAuth authentication. Set a strong token in webmcp.auth.token." \ + "jq '.webmcp.auth = {\"type\": \"token\", \"required\": true}' \"${CONFIG_PATH}\" > \"${CONFIG_PATH}.tmp\" && mv \"${CONFIG_PATH}.tmp\" \"${CONFIG_PATH}\"" + )") + fi + fi + + # Check individual services for auth overrides + local noauth_svcs + noauth_svcs=$(jq -r ' + [(.webmcp.services // [])[] | + select(.auth == null or .auth == false or .auth == "none" or .auth.required == false) | + .name // "unnamed"] | join(", ") + ' "$CONFIG_PATH" 2>/dev/null || true) + + if [[ -n "$noauth_svcs" ]]; then + found_any=true + FINDINGS+=("$(emit_finding \ + "CHK-WEB-007" \ + "warn" \ + "WebMCP service lacks authentication" \ + "The following WebMCP services have no authentication requirement: $noauth_svcs. Unauthenticated services can be invoked by any connected client without verifying identity." \ + "Unauthenticated services: $noauth_svcs" \ + "Add auth requirements to each service: set 'auth.required: true' in the service declaration." \ + "" + )") + fi + + # Check MCP servers for auth + local mcp_noauth + mcp_noauth=$(jq -r ' + [(.mcpServers // {}) | to_entries[]? | + select(.value.auth == null or .value.auth == false) | + .key] | join(", ") + ' "$CONFIG_PATH" 2>/dev/null || true) + + if [[ -n "$mcp_noauth" ]]; then + found_any=true + FINDINGS+=("$(emit_finding \ + "CHK-WEB-007" \ + "warn" \ + "WebMCP service lacks authentication" \ + "MCP server(s) configured without authentication: $mcp_noauth. Connections to these servers are not authenticated, allowing potential MITM or unauthorized access." \ + "MCP servers without auth: $mcp_noauth" \ + "Configure auth for each MCP server connection. Use token-based auth at minimum." \ + "" + )") + fi + fi + + if ! $found_any; then + log_info " No unauthenticated WebMCP services found (or no WebMCP services configured)." + fi +} + +# --------------------------------------------------------------------------- +# CHK-WEB-008: WebMCP declarative form auto-submission risk +# --------------------------------------------------------------------------- +check_form_auto_submission() { + log_info "CHK-WEB-008: Checking for declarative form auto-submission risks..." + + local found_any=false + + if [[ -f "$CONFIG_PATH" ]] && command -v jq &>/dev/null; then + # Check for form-type services with auto-submit enabled + local form_svcs + form_svcs=$(jq -r ' + (.webmcp.services // [])[] | + select(.type == "form" or .type == "declarative-form" or .inputSchema != null) | + "\(.name // "unnamed")|\(.autoSubmit // "null")|\(.confirmRequired // "null")" + ' "$CONFIG_PATH" 2>/dev/null || true) + + while IFS= read -r line; do + [[ -z "$line" ]] && continue + found_any=true + local svc_name auto_submit confirm_required + svc_name="$(echo "$line" | cut -d'|' -f1)" + auto_submit="$(echo "$line" | cut -d'|' -f2)" + confirm_required="$(echo "$line" | cut -d'|' -f3)" + + if [[ "$auto_submit" == "true" ]] || [[ "$auto_submit" != "false" && "$confirm_required" != "true" ]]; then + FINDINGS+=("$(emit_finding \ + "CHK-WEB-008" \ + "warn" \ + "WebMCP declarative form auto-submission risk" \ + "WebMCP form service '$svc_name' may auto-submit data without user confirmation. Declarative forms can be pre-filled by the model and submitted automatically, potentially sending sensitive data to external endpoints." \ + "Service: $svc_name, autoSubmit: $auto_submit, confirmRequired: $confirm_required" \ + "Set 'autoSubmit: false' and 'confirmRequired: true' for all form-type WebMCP services." \ + "" + )") + fi + done <<< "$form_svcs" + + # Check for inputSchema services that could auto-submit + local schema_svcs + schema_svcs=$(jq -r ' + (.webmcp.services // [])[] | + select(.inputSchema != null and (.confirmRequired == null or .confirmRequired == false)) | + .name // "unnamed" + ' "$CONFIG_PATH" 2>/dev/null || true) + + while IFS= read -r svc_name; do + [[ -z "$svc_name" ]] && continue + found_any=true + FINDINGS+=("$(emit_finding \ + "CHK-WEB-008" \ + "warn" \ + "WebMCP declarative form auto-submission risk" \ + "WebMCP service '$svc_name' defines an inputSchema but does not require user confirmation. The agent could auto-fill and submit this form with sensitive data from context." \ + "Service: $svc_name, has inputSchema, confirmRequired not set" \ + "Add 'confirmRequired: true' to services with inputSchema to ensure user reviews before submission." \ + "" + )") + done <<< "$schema_svcs" + fi + + # Scan files for form-like WebMCP declarations + while IFS= read -r file; do + [[ -z "$file" ]] && continue + [[ ! -f "$file" ]] && continue + [[ "$file" == "$CONFIG_PATH" ]] && continue + + local content + content=$(cat "$file" 2>/dev/null || true) + + if echo "$content" | grep -qi "webmcp\|mcpServer" 2>/dev/null; then + if echo "$content" | grep -qiE "auto.?submit|inputSchema|declarative.?form" 2>/dev/null; then + if ! echo "$content" | grep -qi "confirmRequired.*true" 2>/dev/null; then + found_any=true + FINDINGS+=("$(emit_finding \ + "CHK-WEB-008" \ + "warn" \ + "WebMCP declarative form auto-submission risk" \ + "File $file contains WebMCP form declarations without explicit user confirmation requirements. Auto-submitted forms could leak sensitive context data." \ + "File: $file contains form/inputSchema without confirmRequired" \ + "Add 'confirmRequired: true' to all form-type declarations in $file." \ + "" + )") + fi + fi + fi + done < <(gather_webmcp_files) + + if ! $found_any; then + log_info " No form auto-submission risks found." + fi +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- +main() { + log_info "WebMCP Security Scanner starting..." + log_info "Config path: $CONFIG_PATH" + log_info "OpenClaw dir: $OPENCLAW_DIR" + log_info "Deep scan: $CLAWPINCH_DEEP" + + # Verify jq is available + if ! command -v jq &>/dev/null; then + echo '[{"id":"CHK-WEB-000","severity":"critical","title":"jq not found","description":"The jq command is required for JSON parsing but was not found.","evidence":"command -v jq returned non-zero","remediation":"Install jq: brew install jq (macOS) or apt-get install jq (Linux)","auto_fix":""}]' + exit 1 + fi + + # Run all checks + check_untrusted_origins + check_excessive_capabilities + check_model_context_scoping + check_cross_origin_injection + check_data_exfiltration + check_prompt_injection + check_missing_auth + check_form_auto_submission + + # Output all findings as a JSON array + if [[ ${#FINDINGS[@]} -eq 0 ]]; then + echo '[]' + else + printf '%s\n' "${FINDINGS[@]}" | jq -s '.' + fi +} + +main "$@" From 1c6ebfc47e502b05862beefca62e60b59eb3a9fb Mon Sep 17 00:00:00 2001 From: 0xChitlin Date: Tue, 10 Feb 2026 11:53:38 -0500 Subject: [PATCH 2/2] fix: update scanner to match Chrome 146 WebMCP API shape --- scripts/scan_webmcp.sh | 116 +++++++++++++++++++++++++++++++++-------- 1 file changed, 94 insertions(+), 22 deletions(-) diff --git a/scripts/scan_webmcp.sh b/scripts/scan_webmcp.sh index e3509cb..39b527f 100755 --- a/scripts/scan_webmcp.sh +++ b/scripts/scan_webmcp.sh @@ -7,6 +7,13 @@ set -euo pipefail # Scans for WebMCP-related misconfigurations, untrusted origins, excessive # capability grants, prompt injection risks, and data exfiltration vectors. # +# Updated to match the real Chrome 146.0.7651.0 WebMCP API shape: +# - navigator.modelContext.provideContext({ tools: [...] }) (primary) +# - navigator.modelContext.registerTool({ name, description, inputSchema, execute }) +# - navigator.modelContext.unregisterTool(name) +# - navigator.modelContext.clearContext() +# - Each tool requires: name, description, inputSchema, execute (callback) +# # Outputs a JSON array of finding objects to stdout. # # Usage: @@ -146,13 +153,18 @@ gather_webmcp_files() { # - JSON files with "webmcp", "modelContext", "services", "capabilities" # - YAML/YML files with similar content # - Browser extension manifests + # - JS/TS files using the Chrome 146 WebMCP API: + # provideContext, registerTool, unregisterTool, clearContext local found_files=() for dir in "${search_dirs[@]}"; do if [[ -d "$dir" ]]; then while IFS= read -r -d '' f; do found_files+=("$f") done < <(find "$dir" -maxdepth 5 \ - \( -name "*.json" -o -name "*.yaml" -o -name "*.yml" -o -name "*.toml" -o -name "manifest.json" \) \ + \( -name "*.json" -o -name "*.yaml" -o -name "*.yml" -o -name "*.toml" \ + -o -name "manifest.json" \ + -o -name "*.js" -o -name "*.ts" -o -name "*.mjs" -o -name "*.mts" \ + -o -name "*.jsx" -o -name "*.tsx" \) \ -not -path "*/node_modules/*" \ -not -path "*/.git/*" \ -print0 2>/dev/null || true) @@ -185,11 +197,11 @@ check_untrusted_origins() { # Look for webmcp.services, webmcp.endpoints, or mcpServers with URLs local endpoints endpoints=$(jq -r ' - (.webmcp.services // [])[] .origin // empty, - (.webmcp.endpoints // [])[] .origin // empty, - (.webmcp.endpoints // [])[] .url // empty, - (.mcpServers // {}) | to_entries[]? | .value.url // empty, - (.webmcp.trustedOrigins // [])[] // empty + ((.webmcp.services // [])[] | .origin // empty), + ((.webmcp.endpoints // [])[] | .origin // empty), + ((.webmcp.endpoints // [])[] | .url // empty), + ((.mcpServers // {}) | to_entries[]? | .value.url // empty), + ((.webmcp.trustedOrigins // [])[] | select(. != null)) ' "$CONFIG_PATH" 2>/dev/null || true) while IFS= read -r endpoint; do @@ -223,8 +235,8 @@ check_untrusted_origins() { local content content=$(cat "$file" 2>/dev/null || true) - # Look for WebMCP-like URL patterns in JSON files - if echo "$content" | grep -qi "webmcp\|modelContext\|mcpServer" 2>/dev/null; then + # Look for WebMCP-like URL patterns (including Chrome 146 API methods) + if echo "$content" | grep -qiE "webmcp|modelContext|mcpServer|provideContext|registerTool" 2>/dev/null; then # Extract URLs from the file local urls urls=$(echo "$content" | grep -oE '(https?|wss?)://[A-Za-z0-9._~:/?#\[\]@!$&'"'"'()*+,;=-]+' 2>/dev/null || true) @@ -265,8 +277,8 @@ check_excessive_capabilities() { if [[ -f "$CONFIG_PATH" ]] && command -v jq &>/dev/null; then local cap_data cap_data=$(jq -r ' - (.webmcp.services // [])[] | "\(.name // "unnamed"): \(.capabilities // [] | join(","))", - (.mcpServers // {}) | to_entries[]? | "\(.key): \(.value.capabilities // [] | join(","))" + ((.webmcp.services // [])[] | "\(.name // "unnamed"): \((.capabilities // []) | join(","))"), + ((.mcpServers // {}) | to_entries[]? | "\(.key): \((.value.capabilities // []) | join(","))") ' "$CONFIG_PATH" 2>/dev/null || true) while IFS= read -r line; do @@ -303,7 +315,7 @@ check_excessive_capabilities() { content=$(cat "$file" 2>/dev/null || true) if echo "$content" | grep -qi "capabilities\|permissions" 2>/dev/null; then - if echo "$content" | grep -qi "webmcp\|modelContext\|mcpServer" 2>/dev/null; then + if echo "$content" | grep -qiE "webmcp|modelContext|mcpServer|provideContext|registerTool" 2>/dev/null; then for sensitive in "${SENSITIVE_CAPS[@]}"; do if contains_pattern "$content" "\"$sensitive\""; then found_any=true @@ -421,13 +433,14 @@ check_cross_origin_injection() { local found_any=false if [[ -f "$CONFIG_PATH" ]] && command -v jq &>/dev/null; then - # Extract all service declarations with their origins + # Extract all service declarations with their origins (null-safe) local svc_origins svc_origins=$(jq -r ' - [(.webmcp.services // [])[] | {name: (.name // "unnamed"), origin: (.origin // "unknown")}] | + [(.webmcp.services // [])[] | {name: (.name // "unnamed"), origin: (.origin // null // "unknown")}] | + map(select(.origin != null)) | group_by(.origin) | map(select(length > 0) | {origin: .[0].origin, services: [.[].name]}) | - .[] | "\(.origin)|\(.services | join(","))" + .[] | "\(.origin // "unknown")|\(.services | join(","))" ' "$CONFIG_PATH" 2>/dev/null || true) # Check if origins can register services that look like they belong to other origins @@ -501,7 +514,7 @@ check_cross_origin_injection() { local manifest_content manifest_content=$(cat "$manifest" 2>/dev/null || true) - if echo "$manifest_content" | grep -qi "webmcp\|model.context\|mcpServer" 2>/dev/null; then + if echo "$manifest_content" | grep -qiE "webmcp|model\.context|mcpServer|provideContext|registerTool" 2>/dev/null; then found_any=true # Check if extension has cross-origin permissions local has_all_urls @@ -576,7 +589,7 @@ check_data_exfiltration() { local content content=$(cat "$file" 2>/dev/null || true) - if echo "$content" | grep -qi "webmcp\|mcpServer" 2>/dev/null; then + if echo "$content" | grep -qiE "webmcp|mcpServer|provideContext|registerTool|modelContext" 2>/dev/null; then for pattern in "${SENSITIVE_DATA_PATTERNS[@]}"; do if contains_pattern "$content" "$pattern"; then found_any=true @@ -636,11 +649,14 @@ check_prompt_injection() { local found_any=false if [[ -f "$CONFIG_PATH" ]] && command -v jq &>/dev/null; then - # Extract all service descriptions and names + # Extract all service descriptions and tool names + # Covers both config-level service declarations and provideContext-style + # tool registrations (Chrome 146 API: provideContext({ tools: [...] })) local svc_descs svc_descs=$(jq -r ' - (.webmcp.services // [])[] | "\(.name // "unnamed")|\(.description // "")", - (.mcpServers // {}) | to_entries[]? | "\(.key)|\(.value.description // "")" + ((.webmcp.services // [])[] | "\(.name // "unnamed")|\(.description // "")"), + ((.webmcp.tools // [])[] | "\(.name // "unnamed")|\(.description // "")"), + ((.mcpServers // {}) | to_entries[]? | "\(.key)|\(.value.description // "")") ' "$CONFIG_PATH" 2>/dev/null || true) while IFS= read -r line; do @@ -677,8 +693,8 @@ check_prompt_injection() { local content content=$(cat "$file" 2>/dev/null || true) - # Only check files with WebMCP references - if echo "$content" | grep -qi "webmcp\|mcpServer\|modelContext" 2>/dev/null; then + # Only check files with WebMCP references (config or Chrome 146 API) + if echo "$content" | grep -qiE "webmcp|mcpServer|modelContext|provideContext|registerTool" 2>/dev/null; then # Extract description-like fields local descriptions descriptions=$(echo "$content" | grep -oiE '"description"\s*:\s*"[^"]*"' 2>/dev/null || true) @@ -705,6 +721,62 @@ check_prompt_injection() { fi done < <(gather_webmcp_files) + # ----------------------------------------------------------------------- + # Deep scan: Check JS/TS files for prompt injection in execute callbacks + # and provideContext tool registrations (Chrome 146 API). + # + # In the real API, tools are registered via: + # navigator.modelContext.provideContext({ tools: [{ name, description, + # inputSchema, execute: function(input) { ... } }] }) + # navigator.modelContext.registerTool({ name, description, + # inputSchema, execute: function(input) { ... } }) + # + # The execute callback source code can contain prompt injection payloads + # that get returned to the model as tool output. + # ----------------------------------------------------------------------- + if [[ "$CLAWPINCH_DEEP" == "1" ]]; then + log_info " Deep scan: checking JS/TS files for execute callback injection..." + + local js_search_dirs=("$OPENCLAW_DIR") + [[ -d "$WORKSPACE_DIR" ]] && js_search_dirs+=("$WORKSPACE_DIR") + [[ -d "$SKILLS_DIR" ]] && js_search_dirs+=("$SKILLS_DIR") + + for dir in "${js_search_dirs[@]}"; do + [[ ! -d "$dir" ]] && continue + while IFS= read -r -d '' jsfile; do + [[ ! -f "$jsfile" ]] && continue + local jscontent + jscontent=$(cat "$jsfile" 2>/dev/null || true) + + # Look for provideContext or registerTool calls + if echo "$jscontent" | grep -qE '(provideContext|registerTool|modelContext)' 2>/dev/null; then + found_any=true + + # Check for injection patterns in the entire file (covers + # execute callbacks, description strings, and tool names) + for pattern in "${PROMPT_INJECTION_PATTERNS[@]}"; do + if echo "$jscontent" | grep -qiE "$pattern" 2>/dev/null; then + FINDINGS+=("$(emit_finding \ + "CHK-WEB-006" \ + "critical" \ + "WebMCP prompt injection in tool execute callback" \ + "File $jsfile uses the WebMCP API (provideContext/registerTool) and contains prompt injection pattern ('$pattern'). Execute callbacks that return attacker-controlled strings can inject instructions into the model context." \ + "File: $jsfile, Pattern: $pattern" \ + "Audit execute callback return values. Sanitize any user/external data before returning it from tool execute functions. Do not embed instruction-like text in tool output." \ + "" + )") + break # one finding per file + fi + done + fi + done < <(find "$dir" -maxdepth 5 \ + \( -name "*.js" -o -name "*.ts" -o -name "*.mjs" -o -name "*.mts" -o -name "*.jsx" -o -name "*.tsx" \) \ + -not -path "*/node_modules/*" \ + -not -path "*/.git/*" \ + -print0 2>/dev/null || true) + done + fi + if ! $found_any; then log_info " No prompt injection patterns found in service descriptions." fi @@ -860,7 +932,7 @@ check_form_auto_submission() { local content content=$(cat "$file" 2>/dev/null || true) - if echo "$content" | grep -qi "webmcp\|mcpServer" 2>/dev/null; then + if echo "$content" | grep -qiE "webmcp|mcpServer|provideContext|registerTool|modelContext" 2>/dev/null; then if echo "$content" | grep -qiE "auto.?submit|inputSchema|declarative.?form" 2>/dev/null; then if ! echo "$content" | grep -qi "confirmRequired.*true" 2>/dev/null; then found_any=true