Back to skill

Security audit

clawlens

Security checks for vulnerabilities and agentic risk

Overview

Clawlens is a coherent usage-report skill, but it reads private OpenClaw history and stored provider credentials and sends transcript-derived content to external LLMs with weak runtime controls.

Install only if you are comfortable sending selected OpenClaw conversation history, including possible tool-output excerpts, to the chosen LLM provider. Prefer running with a clearly chosen provider, a short --days window, and Markdown output; avoid HTML reports for untrusted session content, and review OpenClaw provider configuration before using auto-detect.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • 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)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
scripts/clawlens.py:828
Finding
External Transmission of Sensitive Conversation Records Without Local Redaction<![CDATA[ ## Vulnerability Details **File Location**: `scripts/clawlens.py:362-371`, `scripts/clawlens.py:556-610`, `scripts/clawlens.py:735-750`, `scripts/clawlens.py:828-832` **Vulnerability Type**: Sensitive-data disclosure to an external LLM provider **Risk Level**: High ### Vulnerable Code ```python def parse_jsonl(filepath: Path) -> list[dict]: """Read a JSONL file, skip malformed lines.""" entries: list[dict] = [] try: with open(filepath, "r", encoding="utf-8") as f: for line_num, line in enumerate(f, 1): line = line.strip() if not line: continue try: entries.append(json.loads(line)) except json.JSONDecodeError: log(f" Skipping malformed line {line_num} in {filepath.name}") ``` ```python def collect_sessions(agent_id: str, days: int, max_sessions: int) -> list[SessionMeta]: """Stage 1: Scan session files and extract metadata.""" base_dir = Path.home() / ".openclaw" / "agents" / agent_id / "sessions" ``` ```python async def llm_call(prompt: str, model: str, max_tokens: int = 4096, retries: int = 3) -> str: """Make an LLM call via litellm with retry.""" kwargs: dict[str, Any] = dict( model=model, messages=[{"role": "user", "content": prompt}], max_tokens=max_tokens, temperature=0.3, ) if _model_api_base: kwargs["api_base"] = _model_api_base if _model_api_key: kwargs["api_key"] = _model_api_key for attempt in range(retries + 1): try: response = await litellm.acompletion(**kwargs) ``` ```python prompt = FACET_EXTRACTION_PROMPT.replace("{transcript}", transcript) try: raw = await llm_call(prompt, model, max_tokens=2048) ``` ### Technical Analysis The skill reads OpenClaw session JSONL files and constructs transcript text containing user messages, assistant messages, tool calls, and excerpts fro ...[truncated 1923 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Require explicit per-run confirmation before transmitting conversation data, including the provider name, destination hostname, session count, and data categories. 2. Implement local redaction before prompt construction. At minimum, detect and remove: - API keys and bearer tokens - OAuth tokens - Private keys and certificates - Passwords and connection strings - Session cookies - Common personal identifiers 3. Exclude tool-result content by default. Provide an explicit option to include it when necessary. 4. Prefer local extraction of metadata and local transcript summarization so that only minimally necessary aggregates leave the device. 5. Add a local-only mode that performs statistical analysis without any external LLM calls. 6. Present a preview of the exact redacted payload or a representative sample before transmission. 7. Document provider retention and privacy implications and allow users to select providers with suitable data-processing guarantees. 8. Apply data minimization at the field level rather than relying only on a character-count limit. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/clawlens.py:287
Finding
Configuration-Controlled API Endpoint Can Receive OpenClaw Credentials and Transcripts<![CDATA[ ## Vulnerability Details **File Location**: `scripts/clawlens.py:287-303`, `scripts/clawlens.py:306-351`, `scripts/clawlens.py:735-750` **Vulnerability Type**: Unvalidated credential forwarding to a configurable endpoint **Risk Level**: High ### Vulnerable Code ```python # --- Look up provider details --- providers = config.get("models", {}).get("providers", {}) if provider_name not in providers: raise RuntimeError( f"Provider '{provider_name}' not found in models.providers of {config_path}" ) provider = providers[provider_name] base_url = provider.get("baseUrl") api_type = provider.get("api") if not base_url: raise RuntimeError(f"baseUrl missing for provider '{provider_name}'") if not api_type: raise RuntimeError(f"api type missing for provider '{provider_name}'") litellm_prefix = _API_TYPE_TO_LITELLM_PREFIX.get(api_type) if not litellm_prefix: raise RuntimeError( f"Unsupported API type '{api_type}' for provider '{provider_name}'. " f"Supported: {list(_API_TYPE_TO_LITELLM_PREFIX.keys())}" ) ``` ```python # --- Read auth credentials --- auth_path = openclaw_dir / "agents" / agent_id / "agent" / "auth-profiles.json" if not auth_path.exists(): raise RuntimeError(f"{auth_path} not found") try: auth_data = json.loads(auth_path.read_text(encoding="utf-8")) except (json.JSONDecodeError, OSError) as e: raise RuntimeError(f"Failed to read {auth_path}: {e}") # Find profile via lastGood last_good = auth_data.get("lastGood", {}) profile_name = last_good.get(provider_name) if not profile_name: raise RuntimeError( f"No lastGood auth profile for provider '{provider_name}' in {auth_path}" ) profiles = auth_data.get("profiles", {}) profile = profiles.get(profile_name) if not profile: raise RuntimeError( f"Auth profile '{profile_name}' not found in {auth_path}" ) auth_type = profile.get("type", "") if auth_type == "api_key": api_key = profile.get("key", "") elif auth_ ...[truncated 3439 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Require HTTPS for all non-local model endpoints and reject plaintext HTTP by default. 2. Maintain an allowlist that binds known provider identifiers to approved hostnames. 3. Store the expected credential audience or origin in the authentication profile and verify it against `baseUrl` before every request. 4. Reject loopback, private, link-local, multicast, and raw-IP destinations unless the user explicitly enables a documented local-provider mode. 5. Display the resolved endpoint and credential profile name and require confirmation for custom or previously unseen endpoints. 6. Do not forward OAuth access tokens to an origin that is not explicitly associated with the token issuer and audience. 7. Normalize and validate URLs to prevent hostname confusion, embedded credentials, redirects, and alternate IP representations. 8. Disable or strictly constrain redirects for requests carrying credentials. 9. Separate custom OpenAI-compatible endpoints from built-in providers and require dedicated credentials for those endpoints. 10. Record non-secret audit events showing which endpoint received data, without logging the credential or transcript. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/clawlens.py:2064
Finding
Unsanitized LLM-Generated Markdown Enables Active Content in HTML Reports<![CDATA[ ## Vulnerability Details **File Location**: `scripts/clawlens.py:2064-2073`, `scripts/clawlens.py:2137-2163`, `scripts/clawlens.py:2167-2174` **Vulnerability Type**: Stored HTML injection through untrusted LLM output **Risk Level**: High ### Vulnerable Code ```python def _md_to_html(md_text: str) -> str: """Convert a Markdown string to HTML. Falls back to <pre> if markdown lib unavailable.""" try: import markdown md = markdown.Markdown(extensions=["tables", "fenced_code"]) result = md.convert(md_text) md.reset() return result except ImportError: # Fallback: wrap in <pre> with basic escaping return f"<pre>{_esc(md_text)}</pre>" ``` ```python # Build section HTML sections_html = [] for i, dim in enumerate(sections, 1): sid = section_ids.get(dim, dim) heading = section_keys_to_labels.get(dim, dim) md_content = dimension_outputs.get(dim, "") content_html = _md_to_html(md_content) # Insert data visuals before LLM prose for relevant sections visuals = "" if dim == "usage_overview": time_charts = "" if hour_chart or weekday_chart: if hour_chart and weekday_chart: time_charts = f'<div class="two-col">{hour_chart}{weekday_chart}</div>' else: time_charts = hour_chart or weekday_chart visuals = stat_cards + model_tags + time_charts elif dim == "task_classification": visuals = task_bars elif dim == "friction_analysis": visuals = friction_split elif dim == "channel_analysis": visuals = channel_bars sections_html.append( f'<section id="{sid}" style="animation-delay:{(i * 0.1):.1f}s;">' f'<h2><span class="num">0{i}</span>{_esc(heading)}</h2>' f'{visuals}' f'<div class="prose">{content_html}</div>' f'</section>' ) ``` ```python # Glance section glance_html = "" if at_a_glance: glance_content = _md_to_h ...[truncated 2516 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Disable raw HTML in LLM-generated Markdown or escape all raw HTML before conversion. 2. Sanitize converted HTML with a maintained allowlist sanitizer such as Bleach or an equivalent library. 3. Permit only necessary structural elements, such as paragraphs, headings, tables, lists, emphasis, and code blocks. 4. Remove all script-capable elements and attributes, including: - `script`, `iframe`, `object`, `embed`, `svg`, and `math` - Attributes beginning with `on` - `style` attributes unless processed by a strict CSS sanitizer - `srcdoc`, `formaction`, and similar navigation attributes 5. Restrict URL schemes to safe values and reject `javascript:`, unsafe `data:` URLs, and unexpected remote resources. 6. Add a restrictive Content Security Policy, for example: - `default-src 'none'` - `style-src 'unsafe-inline'` - `img-src 'self' data:` - `script-src 'self'` only if the built-in script is moved to a controlled local resource, or use a nonce/hash - `connect-src 'none'` - `frame-src 'none'` - `form-action 'none'` 7. Clearly delimit transcript content as untrusted data in LLM prompts and instruct the model never to follow instructions found inside transcripts. 8. Add regression tests using raw HTML, event handlers, dangerous links, SVG payloads, malformed Markdown, and prompt-injected report content. ]]>
Vulnerability Patterns
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (17)

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The description frames the skill as a personal retrospective tool, but the documented behavior includes reading authentication-related files and sending conversation-derived content to an external LLM provider. That mismatch can mislead users into granting access without realizing that sensitive local data and transcript content may be exfiltrated to third parties.

External Model or Provider Selection

High
Category
Excessive Agency
Content
python3 scripts/clawlens.py --lang en --days 7

# Manually specify model (DeepSeek)
DEEPSEEK_API_KEY=sk-xxx python3 scripts/clawlens.py --model deepseek/deepseek-chat

# OpenAI, English, last 7 days
OPENAI_API_KEY=sk-xxx python3 scripts/clawlens.py --model openai/gpt-4o --lang en --days 7
Confidence
96% confidence
Finding
The skill allows user-selected external model/providers and documents sending transcript summaries and session-derived content to those providers. Provider selection expands the data-exfiltration surface and trust boundary, and different providers may have different retention, logging, or jurisdictional risks.

External Model or Provider Selection

High
Category
Excessive Agency
Content
ANTHROPIC_API_KEY=sk-xxx python3 scripts/clawlens.py --model anthropic/claude-sonnet-4-20250514 --verbose -o /tmp/clawlens-report.md

# HTML report (dark-themed, self-contained)
DEEPSEEK_API_KEY=sk-xxx python3 scripts/clawlens.py --model deepseek/deepseek-chat --format html -o /tmp/clawlens-report.html
```

## Output
Confidence
96% confidence
Finding
The HTML-report example reinforces that the skill can package analyzed content and use a selected third-party provider for generation, again extending sensitive data exposure beyond the local environment. The vulnerability is not the example itself but the underlying capability to send user-derived content to arbitrary external LLM services.

Context-Inappropriate Capability

High
Confidence
98% confidence
Finding
The code reads API credentials from local OpenClaw auth profiles and uses them to contact external LLM providers. Accessing secrets and enabling outbound transmission is a major capability expansion over a local retrospective-analysis tool, and it can expose sensitive conversation data to third parties without clear informed consent.

Ssd 3

High
Confidence
99% confidence
Finding
The tool forwards full conversation transcripts, including user text, assistant content, tool calls, and tool results, to an external LLM in plain prompts across multiple pipeline stages. Because transcripts may contain private personal data, credentials, internal file contents, or service responses, this is a significant data exfiltration and privacy risk.

Description-Behavior Mismatch

High
Confidence
99% confidence
Finding
The manifest suggests retrospective analysis of local conversation history, but the implementation sends transcript content to an external LLM for analysis. This is a substantive undisclosed data flow involving potentially sensitive user messages, assistant replies, and tool interactions.

External Model or Provider Selection

High
Category
Excessive Agency
Content
epilog="""Examples:
  python3 clawlens.py --verbose                          # auto-detect model from OpenClaw config
  python3 clawlens.py --lang en --days 7                 # auto-detect, English, last 7 days
  DEEPSEEK_API_KEY=sk-xxx python3 clawlens.py --model deepseek/deepseek-chat
  OPENAI_API_KEY=sk-xxx python3 clawlens.py --model openai/gpt-4o --lang en --days 7
  ANTHROPIC_API_KEY=sk-xxx python3 clawlens.py --model anthropic/claude-sonnet-4-20250514 --verbose -o report.md
  DEEPSEEK_API_KEY=sk-xxx python3 clawlens.py --model deepseek/deepseek-chat --format html -o report.html
Confidence
90% confidence
Finding
Skill selects an external model or provider that may use a different account or billing plan than the operator expects. Undisclosed model switches can cause unexpected cost or quota consumption.

External Model or Provider Selection

High
Category
Excessive Agency
Content
DEEPSEEK_API_KEY=sk-xxx python3 clawlens.py --model deepseek/deepseek-chat
  OPENAI_API_KEY=sk-xxx python3 clawlens.py --model openai/gpt-4o --lang en --days 7
  ANTHROPIC_API_KEY=sk-xxx python3 clawlens.py --model anthropic/claude-sonnet-4-20250514 --verbose -o report.md
  DEEPSEEK_API_KEY=sk-xxx python3 clawlens.py --model deepseek/deepseek-chat --format html -o report.html
""",
    )
    parser.add_argument("--agent-id", default="main", help="Agent ID (default: main)")
Confidence
90% confidence
Finding
Skill selects an external model or provider that may use a different account or billing plan than the operator expects. Undisclosed model switches can cause unexpected cost or quota consumption.

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill declares powerful capabilities in metadata (file reads/writes and external API use) but does not define any explicit tool scope or permission boundary. This increases the chance of over-broad execution and weak user understanding of what the skill can access, especially because it reads session history, skills directories, config, and auth-profile files.

Vague Triggers

Medium
Confidence
92% confidence
Finding
The trigger phrases are broad enough to match ordinary conversation, which can cause accidental invocation of a skill that reads private history and transmits analysis data externally. In this context, ambiguous activation is more dangerous because the skill handles sensitive conversation logs and may auto-select a model/provider.

Natural-Language Policy Violations

Medium
Confidence
97% confidence
Finding
The instructions say the agent "must ask the user" using a fixed Chinese prompt, which imposes a language choice regardless of user preference. This is a natural-language policy concern because the skill does not offer opt-in or an alternate localized prompt for users who are not Chinese speakers.

Natural-Language Policy Violations

Medium
Confidence
94% confidence
Finding
The sample report structure is defined with Chinese headings and content labels, which establishes a default output locale. Because the file does not present this as optional or region-specific, it can conflict with a policy requiring language choice rather than forcing a locale.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
When transcripts are long, the tool chunks and uploads them for summarization without any specific disclosure. Chunking does not reduce the privacy risk; it still transfers sensitive content externally and may increase exposure by making multiple requests.

Missing User Warnings

Medium
Confidence
97% confidence
Finding
Report generation uses aggregated session data and previously extracted content derived from transcripts, but there is no user-facing warning that conversation contents are sent to an external LLM. This creates a transparency and consent failure around sensitive data handling.

Description-Behavior Mismatch

Medium
Confidence
93% confidence
Finding
The skill claims to analyze conversation history, but it also inventories globally installed skills from ~/.openclaw/skills. That expands data collection beyond the stated purpose and can reveal unrelated local environment information, violating least-privilege and user expectations.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The CLI sets `--lang` default to `zh`, so the tool will produce Chinese output unless the user overrides it. Because the default forces a specific language rather than asking or inferring user preference with opt-in, it can violate language/locale choice expectations.

Missing User Warnings

Low
Confidence
88% confidence
Finding
The script accesses local auth-profiles.json to retrieve API keys or OAuth tokens without clearly notifying the user. Even if used only for intended API calls, silent secret access is risky because it broadens trust requirements and may surprise users reviewing a skill described as analytics/reporting.

Static analysis

No suspicious patterns detected.