Back to skill

Security audit

External Receiver

Security checks for vulnerabilities and agentic risk

Overview

This skill does what it says, but its default network receiver can expose uploaded files and inject external messages into an OpenClaw session with weak access controls.

Install only if you are prepared to harden it first: bind it to localhost or a trusted interface, require a strong secret, protect or remove the status and download endpoints, add request size limits, and treat all received content as untrusted before it reaches an OpenClaw session.

Vulnerability Patterns
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • 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
  • 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 (4)

T01 · Skill Instruction Hijacking

Error
Location
receiver_server.py:22
Finding
Unauthenticated Network Input Is Forwarded into the OpenClaw Agent Session<![CDATA[ ## Vulnerability Details **File Location**: `receiver_server.py:22-25, 33-48, 119-123, 196-207, 252-305`; `scripts/start.sh:8-10, 25-33` **Vulnerability Type**: Unauthenticated agent instruction injection **Risk Level**: High ### Vulnerable Code ```python RECEIVER_HOST = os.getenv("RECEIVER_HOST", "0.0.0.0") RECEIVER_PORT = int(os.getenv("RECEIVER_PORT", "8080")) RECEIVER_DIR = Path(os.getenv("RECEIVER_DIR", os.path.join(os.path.dirname(__file__), "../received"))) RECEIVER_SECRET = os.getenv("RECEIVER_SECRET", "") # optional secret ``` ```python def push_to_openclaw(text: str): queue_file = Path.home() / ".openclaw" / "workspace" / "received" / "message_queue.jsonl" queue_file.parent.mkdir(parents=True, exist_ok=True) entry = { "time": datetime.now().isoformat(), "text": text, } queue_file.append_text(json.dumps(entry, ensure_ascii=False) + "\n") log(f"Message written to queue: {queue_file}") _try_ws_push(text) ``` ```python def _check_secret(self) -> bool: if not RECEIVER_SECRET: return True auth = self.headers.get("Authorization", "") return auth == f"Bearer {RECEIVER_SECRET}" ``` ```python payload = _json.dumps({ "jsonrpc": "2.0", "method": "sessions.send", "params": { "text": text, }, "id": 1 }).encode("utf-8") ``` ```python def do_POST(self): if not self._check_secret(): self._error("Unauthorized", 401) return if self.path == "/upload": self._handle_upload() elif self.path == "/message": self._handle_message() elif self.path == "/webhook": self._handle_webhook() else: self._error("Unknown path", 404) ``` ```bash PORT="${RECEIVER_PORT:-8080}" HOST="${RECEIVER_HOST:-0.0.0.0}" SECRET="${RECEIVER_SECRET:-}" ARGs=(--port "$PORT" --host "$HOST") if [ -n "$SECRET" ]; then ARGs+=(--secret "$SECRET") fi python3 receiver_server.py "${ARGs[@]}" ``` ### Technical Analysis The re ...[truncated 2338 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Fail closed when no authentication secret is configured. Refuse to start on a non-loopback interface unless a strong secret or equivalent authentication mechanism is present. 2. Change the default host from `0.0.0.0` to `127.0.0.1`. 3. Generate a high-entropy credential during installation rather than relying on an optional environment variable. 4. Compare bearer tokens with `hmac.compare_digest()` and return a generic authorization failure. 5. Place the service behind TLS; bearer credentials transmitted over plaintext HTTP can otherwise be intercepted. 6. Apply per-client rate limiting, request quotas, and audit logging. 7. Mark all received content as untrusted external data using a structured envelope. Downstream Agent logic must not interpret it as system, developer, or operator instructions. 8. Require explicit operator approval before external content can initiate tool calls or privileged actions. 9. Consider disabling direct `sessions.send` forwarding and exposing received messages only through a quarantined review interface. 10. Update `scripts/start.sh` and `SKILL.md` so that authenticated, loopback-only operation is the documented and enforced default. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
receiver_server.py:145
Finding
Authentication Is Bypassed for File Listing and Download Endpoints<![CDATA[ ## Vulnerability Details **File Location**: `receiver_server.py:145-178, 196-207` **Vulnerability Type**: Missing authorization on sensitive GET endpoints **Risk Level**: High ### Vulnerable Code ```python def do_GET(self): if self.path == "/health": self._ok(status="running", time=datetime.now().isoformat()) return if self.path.startswith("/download/"): filename = os.path.basename(self.path[10:]) filepath = RECEIVER_DIR / filename if not filepath.is_file(): self._error("File does not exist", 404) return self.send_response(200) self.send_header("Content-Type", "application/octet-stream") self.send_header("Content-Disposition", f'attachment; filename="{filename}"') self.end_headers() with open(filepath, "rb") as f: self.wfile.write(f.read()) return files = list(RECEIVER_DIR.iterdir()) html = f"""<!DOCTYPE html> <html><head><meta charset="utf-8"><title>External Receiver</title></head> <body> <h1>External Receiver</h1> <p>Status: running</p> <p>Time: {datetime.now().isoformat()}</p> <p>File count: {len(files)}</p> <p>Port: {RECEIVER_PORT}</p> <h2>Recent files</h2><ul> {"".join(f'<li><a href="/download/{f.name}">{f.name}</a> ({f.stat().st_size}B)</li>' for f in files[:10])} </ul> ... </body></html>""" ``` Authentication is applied only in the POST handler: ```python def do_POST(self): if not self._check_secret(): self._error("Unauthorized", 401) return if self.path == "/upload": self._handle_upload() elif self.path == "/message": self._handle_message() elif self.path == "/webhook": self._handle_webhook() else: self._error("Unknown path", 404) ``` ### Technical Analysis The authorization check is implemented exclusively in `do_POST()`. The `do_GET()` handler serves the status page and files without calling `_check_secret()`. This remains true ...[truncated 1521 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Apply authentication before routing every endpoint other than a deliberately minimal health endpoint. 2. If `/health` must remain public, return only a generic status and do not expose timestamps, paths, ports, file counts, or configuration details. 3. Remove public directory enumeration from the root page. 4. Replace filesystem names in download URLs with cryptographically random, non-guessable identifiers. 5. Enforce per-object authorization rather than assuming possession of a filename grants access. 6. Set cache-control headers appropriate for confidential downloads. 7. Return downloads through bounded streaming instead of reading an entire file into memory. 8. Add authorization tests covering `GET /`, `GET /download/*`, POST endpoints, malformed credentials, and configurations with and without a secret. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
receiver_server.py:167
Finding
Uploaded Filenames Cause Stored HTML Injection on the Status Page<![CDATA[ ## Vulnerability Details **File Location**: `receiver_server.py:167-168, 220-231` **Vulnerability Type**: Stored cross-site scripting through an unescaped filename **Risk Level**: Medium ### Vulnerable Code The attacker-supplied basename is retained in the stored filename: ```python file_item = form["file"] if not file_item.filename: self._error("Filename is empty") return filename = os.path.basename(file_item.filename) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") safe_name = f"{timestamp}_{filename}" filepath = RECEIVER_DIR / safe_name data = file_item.file.read() filepath.write_bytes(data) ``` The resulting filename is interpolated into HTML and an `href` without HTML escaping or URL encoding: ```python files = list(RECEIVER_DIR.iterdir()) html = f"""<!DOCTYPE html> <html><head><meta charset="utf-8"><title>External Receiver</title></head> <body> <h1>External Receiver</h1> <p>Status: running</p> <p>Time: {datetime.now().isoformat()}</p> <p>File count: {len(files)}</p> <p>Port: {RECEIVER_PORT}</p> <h2>Recent files</h2><ul> {"".join(f'<li><a href="/download/{f.name}">{f.name}</a> ({f.stat().st_size}B)</li>' for f in files[:10])} </ul> ... </body></html>""" ``` ### Technical Analysis `os.path.basename()` only removes directory components. It does not make a filename safe for use in HTML text or an HTML attribute. Characters such as quotation marks, angle brackets, and ampersands remain attacker-controlled. The timestamp prefix does not neutralize the dangerous suffix. When the root status page is rendered, the uploaded name is inserted into both the anchor's `href` attribute and visible HTML without context-sensitive encoding. A crafted filename can terminate the attribute or element and insert new markup or script-capable content. The payload is stored on disk and remains active whenever the status page includes that file among the displayed entries. ### Attack Path 1. The attacker reaches the upload endpoint and, where req ...[truncated 1185 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Do not preserve arbitrary client filenames as server-side identifiers. Generate a random server filename and store the original name only as metadata. 2. Restrict accepted original filenames to a conservative allowlist of letters, digits, spaces, dots, underscores, and hyphens. 3. Escape every displayed filename using `html.escape(name, quote=True)`. 4. Encode path components with `urllib.parse.quote()` before including them in URLs. 5. Prefer a template engine with automatic contextual escaping rather than constructing HTML with f-strings. 6. Add a restrictive Content Security Policy, such as disallowing inline scripts, as defense in depth. 7. Set `X-Content-Type-Options: nosniff` and appropriate framing restrictions. 8. Add regression tests using filenames containing quotation marks, angle brackets, ampersands, Unicode edge cases, and URL delimiters. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
receiver_server.py:211
Finding
Unbounded Request Buffering Permits Memory and Disk Exhaustion<![CDATA[ ## Vulnerability Details **File Location**: `receiver_server.py:211-235, 252-254, 278-280` **Vulnerability Type**: Denial of service through unrestricted request sizes **Risk Level**: High ### Vulnerable Code Uploads are read completely into memory and then copied to disk: ```python form = cgi.FieldStorage( fp=self.rfile, headers=self.headers, environ={"REQUEST_METHOD": "POST"} ) if "file" not in form: self._error("File field not found") return file_item = form["file"] if not file_item.filename: self._error("Filename is empty") return filename = os.path.basename(file_item.filename) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") safe_name = f"{timestamp}_{filename}" filepath = RECEIVER_DIR / safe_name data = file_item.file.read() filepath.write_bytes(data) size = len(data) ``` Message and webhook bodies trust the supplied content length and allocate accordingly: ```python length = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(length).decode("utf-8") ``` ```python length = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(length).decode("utf-8") data = json.loads(body) ``` Downloads also read an entire file before writing it to the client: ```python with open(filepath, "rb") as f: self.wfile.write(f.read()) ``` ### Technical Analysis No maximum upload size, message size, webhook size, per-client quota, storage quota, or request timeout is enforced. The upload path reads the full uploaded file into a Python byte string before writing it to disk, producing memory consumption proportional to the upload size in addition to multipart parser overhead. The message and webhook handlers parse an attacker-controlled `Content-Length` and pass it directly to `read()`. Very large bodies can consume excessive memory. Slow or incomplete clients can also occupy the single request-processing thread because the server uses `HTTPServer`, not a hardened concurrent production server wit ...[truncated 1508 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Define strict maximum sizes for uploads, messages, and webhooks. 2. Validate `Content-Length` before reading the body and return HTTP `413 Payload Too Large` when it exceeds the applicable limit. 3. Stream uploads to disk in bounded chunks rather than calling an unbounded `read()`. 4. Track the cumulative number of bytes and abort immediately when the limit is exceeded, including for requests without a trustworthy content length. 5. Enforce per-file, per-client, and total-directory quotas. 6. Store uploads on a dedicated filesystem or volume with a fixed quota and restrictive permissions. 7. Stream downloads in bounded chunks. 8. Configure connection, header, body-read, and idle timeouts. 9. Add rate limiting and limits on concurrent connections. 10. Deploy behind a hardened reverse proxy that enforces request-size and timeout limits. 11. Replace the development-style `HTTPServer` with an appropriately configured production server. 12. Clean up partially written files after failed or aborted uploads and implement a controlled retention policy. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (16)

Lp3

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding
The skill declares an HTTP receiver that reads environment variables, accepts network input, and writes uploaded files, but it does not declare any explicit tool scope or permissions. This weakens reviewability and consent because users cannot clearly see that the skill exposes network, file-write, and data-ingestion capabilities before installation or use.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The skill is explicitly designed to expose an HTTP service that receives external files and messages and automatically forwards them into an OpenClaw session, yet the description does not prominently warn about privacy, prompt-injection, or untrusted-content risks. Because this creates a direct ingestion path from external actors into a user session, it can enable data poisoning, social engineering, or malicious content delivery.

External Transmission

Medium
Category
Data Exfiltration
Content
### 上传文件

```bash
curl -X POST http://你的服务器:8080/upload \
  -F "file=@/path/to/file.txt"
```
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
import requests

# 发送消息
requests.post("http://服务器:8080/message", data={"text": "警报:价格突破"})

# 上传文件
with open("report.pdf", "rb") as f:
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
import requests

# 发送消息
requests.post("http://服务器:8080/message", data={"text": "警报:价格突破"})

# 上传文件
with open("report.pdf", "rb") as f:
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
import requests

# 发送消息
requests.post("http://服务器:8080/message", data={"text": "警报:价格突破"})

# 上传文件
with open("report.pdf", "rb") as f:
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
requests.post("http://服务器:8080/upload", files={"file": f})

# Webhook 方式
requests.post("http://服务器:8080/webhook", json={
    "event": "trade",
    "symbol": "BTC/USDT",
    "side": "buy",
Confidence
70% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Missing User Warnings

Medium
Confidence
87% confidence
Finding
This code performs a destructive file write by emptying the queue file after reading messages. Although the module docstring says it 'reads and clears' the queue, the function itself provides no runtime warning, confirmation, or visible logging when this irreversible action occurs, which can surprise callers and lead to data loss.

Context-Inappropriate Capability

Medium
Confidence
88% confidence
Finding
The code reads the local OpenClaw gateway configuration and authentication token, then uses that token to create its own push channel into local sessions. While likely intended for integration, it grants this HTTP-facing service the ability to inject content into local OpenClaw context, so any compromise or misuse of the receiver can pivot into the user's session.

Description-Behavior Mismatch

Medium
Confidence
96% confidence
Finding
The skill is described as a one-way external receiver, but it also exposes a browsable status page and a file download endpoint that expand it into a data-serving interface. This increases attack surface and can leak uploaded files and metadata to anyone who can reach the server, especially because GET routes are not protected by the shared secret.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
The /download/<filename> endpoint is reachable without authentication because secret checks are only applied in POST handlers. That allows any reachable client to retrieve uploaded files by name, and the status page helps enumerate recent filenames, turning this into a straightforward information disclosure issue.

Missing User Warnings

Medium
Confidence
83% confidence
Finding
Incoming message and webhook contents are forwarded into OpenClaw via a queue file and attempted WebSocket push, but the code does not clearly disclose near these handlers that externally supplied data will be relayed onward. Since this is a network/data-transmission behavior affecting privacy and downstream handling, a user-facing warning or explicit comment/docstring should be present.

Natural-Language Policy Violations

Low
Confidence
88% confidence
Finding
The manifest description and the substantive instructions in the markdown are presented in Chinese, and the file does not indicate that users may choose another language or that the skill is intentionally limited to a Chinese-speaking audience. This can violate a language/locale policy when a specific language is imposed without user opt-in.

Natural-Language Policy Violations

Low
Confidence
95% confidence
Finding
The module docstring and user-visible CLI output are written only in Chinese, which imposes a specific language without any user opt-in or documented locale constraint. Under the policy, hard-coded language requirements should either provide a choice or clearly justify the locale restriction.

Natural-Language Policy Violations

Low
Confidence
92% confidence
Finding
The natural-language strings in the module docstring and operational messages are presented only in Chinese, with no indication that the user can select another language or locale. This can violate language/locale policy when a skill implicitly forces one language without opt-in or justification.

Natural-Language Policy Violations

Low
Confidence
96% confidence
Finding
The script's user-facing text is written in Chinese, including the title and runtime status/error messages. This imposes a specific language on all users without any opt-in, fallback, or documented region-specific justification, which matches the language/locale policy violation criteria.

Static analysis

No suspicious patterns detected.