Back to skill

Security audit

WebChat HTTPS Proxy

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly does what it advertises, but its persistent HTTPS proxy has weak authentication that can expose the local transcription service if LAN access is enabled.

Review this before installing if you plan to expose the proxy beyond localhost. It creates a persistent user systemd service and changes OpenClaw allowed origins. Keep VOICE_HOST on 127.0.0.1 unless you understand the LAN risk, and avoid putting gateway tokens in URLs or shell commands where they may be logged. This does not look malicious, but the authentication weakness should be fixed before trusting it on a shared network.

Vulnerability Patterns
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Agent Memory PoisoningWrites attacker-controlled rules into memory that affect later sessions
  • Remote Payload Retrieval and ExecutionFetches external code whose behavior can change after review
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (1)

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
assets/https-server.py:84
Finding
Authentication Bypass on the Transcription Proxy Endpoint<![CDATA[ ## Vulnerability Details **File Location**: `assets/https-server.py`, lines 84-104 **Vulnerability Type**: Authentication bypass caused by trusting client-controlled HTTP headers **Risk Level**: Medium ### Vulnerable Code ```python def _check_auth(request): """Allow same-origin browser requests; optionally accept gateway Bearer token.""" origin = request.headers.get("Origin", "") referer = request.headers.get("Referer", "") if origin == ALLOWED_ORIGIN: return None if referer.startswith(ALLOWED_ORIGIN + "/") or referer == ALLOWED_ORIGIN: return None gateway_token = _read_gateway_token() if not gateway_token: # No gateway token configured — allow (localhost-only safe default) return None auth_header = request.headers.get("Authorization", "") if auth_header.startswith("Bearer "): provided = auth_header[7:].strip() if hmac.compare_digest(provided, gateway_token): return None return web.json_response( {"error": "unauthorized"}, status=401, headers={"Access-Control-Allow-Origin": ALLOWED_ORIGIN}, ) ``` The vulnerable authentication function is invoked by the transcription endpoint: ```python async def handle_transcribe(request): auth_err = _check_auth(request) if auth_err is not None: return auth_err ``` ### Technical Analysis The proxy treats a matching `Origin` or `Referer` header as sufficient proof that a request is authenticated. These headers can help enforce browser-origin policy, but they are not authentication credentials. Any non-browser HTTP client can set either header to an arbitrary value. Consequently, an attacker does not need the configured gateway Bearer token. The attacker can set `Origin` to the known public origin of the proxy, such as `https://10.0.0.42:8443`, and `_check_auth()` will immediately authorize the request. There is a second fail-open condition: if the gateway token is abs ...[truncated 2339 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. **Do not use `Origin` or `Referer` as authentication.** Retain origin validation only as a browser security control, separate from identity verification. 2. **Require valid credentials for every `/transcribe` request.** Validate a Bearer token, authenticated session, or narrowly scoped service token regardless of whether the request appears same-origin. 3. **Fail closed when authentication configuration is unavailable.** Missing, unreadable, or malformed gateway configuration should produce a startup failure or an HTTP 503/401 response instead of allowing unauthenticated access. 4. **Apply stricter requirements to non-loopback listeners.** Refuse to start on a non-loopback address unless authentication is configured successfully. 5. **Use CSRF protection for session-based browser access.** If browser sessions are introduced, require an unpredictable CSRF token in addition to validating the exact origin. A safer authorization structure would be: ```python def _check_auth(request): gateway_token = _read_gateway_token() if not gateway_token: return web.json_response( {"error": "authentication unavailable"}, status=503, headers={"Access-Control-Allow-Origin": ALLOWED_ORIGIN}, ) auth_header = request.headers.get("Authorization", "") if not auth_header.startswith("Bearer "): return web.json_response( {"error": "unauthorized"}, status=401, headers={"Access-Control-Allow-Origin": ALLOWED_ORIGIN}, ) provided = auth_header[7:].strip() if not hmac.compare_digest(provided, gateway_token): return web.json_response( {"error": "unauthorized"}, status=401, headers={"Access-Control-Allow-Origin": ALLOWED_ORIGIN}, ) return None ``` Origin validation may still be performed independently for browser requests, but a matching origin must never bypass creden ...[truncated 20 chars]
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Tool MisuseTool Parameter Abuse, Chaining Abuse, Unsafe Defaults
  • Rogue AgentSelf-Modification, Session Persistence
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
Findings (14)

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

Critical
Category
Data Flow
Content
loop = asyncio.get_event_loop()
        resp = await loop.run_in_executor(
            None,
            lambda: urllib.request.urlopen(req, timeout=120),
        )
        data = resp.read(MAX_RESPONSE_BODY + 1)
        if len(data) > MAX_RESPONSE_BODY:
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
echo "[1/4] Stopping and removing openclaw-voice-https.service..."
systemctl --user stop openclaw-voice-https.service 2>/dev/null || true
systemctl --user disable openclaw-voice-https.service 2>/dev/null || true
rm -f "$HOME/.config/systemd/user/openclaw-voice-https.service"
systemctl --user daemon-reload 2>/dev/null || true
echo "      done."
Confidence
95% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Tool Parameter Abuse

High
Category
Tool Misuse
Content
rm -rf "$CERT_DIR"
  echo "      removed: $CERT_DIR"
fi
rm -f "$VOICE_DIR/https-server.py"
rmdir "$VOICE_DIR" 2>/dev/null && echo "      removed: $VOICE_DIR" || true

echo ""
Confidence
95% confidence
Finding
Tool parameters are crafted to achieve unintended or unsafe behavior. Parameter abuse can bypass intended safety checks (e.g. shell=True, --force, dangerous glob patterns).

Lp3

Medium
Category
MCP Least Privilege
Confidence
95% confidence
Finding
The skill advertises meaningful capabilities including environment access, file modification, shell execution, persistence, and network exposure, but does not declare any explicit tool scope such as permissions or allowed-tools. This weakens reviewability and policy enforcement because operators cannot easily constrain what the skill is allowed to do, especially given that it installs a persistent HTTPS proxy and modifies config files.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
- VOICE_ALLOWED_ORIGIN (optional, default: https://<VOICE_HOST>:<VOICE_HTTPS_PORT>)
  persistence:
    - "User systemd service: openclaw-voice-https.service (HTTPS/WSS proxy)"
  privileges: user-level only, no root/sudo required
  dependencies:
    - python3 with aiohttp >= 3.9.0 (pip)
    - openssl (for self-signed cert generation)
Confidence
70% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Sudo/Root Execution

Medium
Category
Privilege Escalation
Content
### TLS
- **TLS 1.2+ enforced**: Minimum protocol version set to TLS 1.2. Legacy SSL/TLS 1.0/1.1 rejected.
- **Self-signed TLS**: Auto-generated certificate. Browser certificate warning on first access.
- **Private key permissions**: `chmod 600` on TLS key file.

### Authentication
- **Bearer token auth**: `/transcribe` endpoint validates Bearer token against gateway auth token using constant-time comparison (`hmac.compare_digest`).
Confidence
80% confidence
Finding
Commands invoke sudo or root privileges. Verify this elevated access is necessary and justified.

Session Persistence

Medium
Category
Rogue Agent
Content
- No telemetry, analytics, or phone-home behavior.

### Persistence
- User systemd service starts on boot. Use `uninstall.sh` to fully revert.

## What this skill modifies
Confidence
91% confidence
Finding
The skill establishes persistence via a user systemd service that starts automatically on boot/login. Persistence is not inherently malicious in infrastructure software, but it is security-relevant because it creates a long-lived network-facing proxy whose exposure may outlast user awareness, particularly if VOICE_HOST is set to a LAN IP.

Session Persistence

Medium
Category
Rogue Agent
Content
| What | Path | Action |
|---|---|---|
| Gateway config | `~/.openclaw/openclaw.json` | Adds HTTPS origin to `gateway.controlUi.allowedOrigins` |
| Systemd service | `~/.config/systemd/user/openclaw-voice-https.service` | Creates + enables persistent HTTPS proxy |
| Runtime file | `~/.openclaw/workspace/voice-input/https-server.py` | Copies proxy server |
| TLS certs | `~/.openclaw/workspace/voice-input/certs/` | Auto-generated self-signed cert on first run |
Confidence
93% confidence
Finding
Creating and enabling a persistent user-level systemd unit is a genuine persistence mechanism. In this skill's context the purpose appears operational, but the behavior still increases attack surface because it ensures the HTTPS proxy remains active across sessions and may continue exposing local services if misconfigured.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
return explicit
    try:
        # Safe: fixed argument list, no user input, no shell=True
        npm_root = subprocess.check_output(["npm", "-g", "root"], text=True, stderr=subprocess.DEVNULL).strip()
        candidate = os.path.join(npm_root, "openclaw", "dist", "control-ui")
        if os.path.isdir(candidate):
            return candidate
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

subprocess module call

Medium
Category
Dangerous Code Execution
Content
return

    # Safe: fixed argument list, no user input, no shell=True
    subprocess.run([
        "openssl", "req", "-x509", "-nodes", "-newkey", "rsa:2048",
        "-keyout", str(key_path),
        "-out", str(cert_path),
Confidence
70% confidence
Finding
subprocess module calls execute external commands. Without careful input validation, this enables command injection.

Tainted flow: 'key_path' from os.environ.get (line 293, credential/environment) → subprocess.run (code execution)

Medium
Category
Data Flow
Content
return

    # Safe: fixed argument list, no user input, no shell=True
    subprocess.run([
        "openssl", "req", "-x509", "-nodes", "-newkey", "rsa:2048",
        "-keyout", str(key_path),
        "-out", str(cert_path),
Confidence
65% confidence
Finding
Data from a source is assigned to a variable that is later passed to a sink, creating a variable-mediated taint flow.

Unsafe Defaults

Medium
Category
Tool Misuse
Content
"-subj", "/CN=openclaw-voice-local",
    ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

    # SECURITY: Ensure private key is not group/world-readable
    os.chmod(str(key_path), 0o600)
Confidence
70% confidence
Finding
Tool defaults are unsafe or overly permissive (e.g. disabled TLS verification, no authentication, world-writable permissions). Unsafe defaults widen the attack surface.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The troubleshooting guidance instructs users to place gateway tokens directly in a URL query string and in CLI arguments. These tokens can be exposed through browser history, server and proxy logs, shell history, process listings, and screenshots or copied commands, increasing the chance of credential leakage and unauthorized access to the gateway.

Session Persistence

Medium
Category
Rogue Agent
Content
UNIT

systemctl --user daemon-reload
systemctl --user enable --now openclaw-voice-https.service
systemctl --user restart openclaw-voice-https.service

# 5) Restart gateway so allowedOrigins applies
Confidence
80% confidence
Finding
Skill establishes unauthorized persistence across sessions via cron jobs, startup scripts, or state files. Session persistence allows an attacker to maintain access beyond the current interaction.

Static analysis

No suspicious patterns detected.