Back to skill

Security audit

Webhook Promo Scheduler

Security checks for vulnerabilities and agentic risk

Overview

The skill mostly matches its Discord posting purpose, but its webhook credential handling and destination controls need review before use.

Install only if you are comfortable reviewing and controlling how it is invoked. Do not pass real Discord webhook URLs directly on shared shells, CI logs, or scheduler command lines; use a protected secret mechanism or wrapper. Restrict the webhook destination to Discord before exposing this through automation, and do not rely on the ledger as a hard anti-spam guarantee under concurrent jobs.

Vulnerability Patterns
  • 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
  • Embedded Malicious CodeShips malicious scripts inside the skill and executes them locally
Findings (3)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/promo_scheduler.py:137
Finding
Webhook Credential Exposure Through Command-Line Arguments<![CDATA[ ## Vulnerability Details **File Location**: `scripts/promo_scheduler.py:137` and `SKILL.md:40-43,48-52` **Vulnerability Type**: Secret exposure through process arguments and shell history **Risk Level**: Medium ### Vulnerable Code ```python p_post.add_argument("--webhook-url", required=True, help="Discord webhook URL (never printed)") ``` The documented invocation also instructs users to provide the secret directly on the command line: ```bash python3 {baseDir}/scripts/promo_scheduler.py post \ --webhook-url <URL> \ --channel openclaw-discord \ --message "Hello from OpenClaw!" ``` ```bash python3 {baseDir}/scripts/promo_scheduler.py rotate \ --webhook-url <URL> \ --channel openclaw-discord \ --messages-file messages.txt ``` ### Technical Analysis A Discord webhook URL contains authentication material and should be treated as a credential. Although the application redacts the value from its own status messages, accepting it as a command-line argument exposes it outside the application's logging controls. Depending on the operating environment, command-line arguments may be retained or exposed through: - Interactive shell history. - Process listings and process-inspection interfaces. - CI/CD job metadata and command logs. - Scheduler definitions and service configuration. - Endpoint monitoring and process telemetry. - Debugging or diagnostic data. Consequently, the claim that the URL is “never printed” does not provide end-to-end protection for the credential. Application-level output redaction cannot prevent exposure by the shell or operating system. ### Attack Path 1. A user follows the documented example and supplies the Discord webhook URL through `--webhook-url`. 2. The shell records the command in its history, or the operating system exposes the argument through process inspection. 3. A local user, monitoring service, CI operator, or attacker with access to those records obtains the webhook URL. 4. The attacker sends reque ...[truncated 664 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Prefer a protected environment variable, such as `DISCORD_WEBHOOK_URL`, rather than a command-line option. - Support reading the credential from a permission-restricted file or standard input. - For interactive execution, use `getpass.getpass()` or another non-echoing input mechanism. - If `--webhook-url` is retained for compatibility, mark it as insecure and deprecated. - Update all usage examples so they do not place a real webhook URL in shell history. - Document appropriate secret rotation procedures and recommend immediate rotation after suspected exposure. - In CI/CD environments, use the platform's secret store and ensure command tracing is disabled while secrets are loaded. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/post_webhook.py:36
Finding
Unrestricted Webhook Destination Enables Arbitrary Outbound Requests<![CDATA[ ## Vulnerability Details **File Location**: `scripts/post_webhook.py:36-47` **Vulnerability Type**: Missing destination validation and SSRF-like outbound request behavior **Risk Level**: Medium ### Vulnerable Code ```python req = urllib.request.Request( webhook_url, data=data, headers={ "Content-Type": "application/json", "User-Agent": "openclaw-webhook-promo-scheduler/1.0", }, method="POST", ) try: with urllib.request.urlopen(req, timeout=timeout_seconds) as resp: code = getattr(resp, "status", None) or resp.getcode() ``` The destination enters the program without validation: ```python p_post.add_argument("--webhook-url", required=True, help="Discord webhook URL (never printed)") ``` ### Technical Analysis The application describes the supplied destination as a Discord webhook, but passes it directly to `urllib.request.Request` and `urllib.request.urlopen`. It does not enforce: - The HTTPS scheme. - An approved Discord hostname. - The expected `/api/webhooks/` path. - An approved destination port. - The absence of embedded URL credentials. - Rejection of loopback, private, link-local, or reserved IP addresses. - Validation of redirect destinations. As a result, any party able to influence the `--webhook-url` argument can cause the process to issue an outbound request to an arbitrary URL reachable from the host. The body is JSON containing attacker- or user-supplied message content and an optional username. This is an SSRF-like primitive when invocation parameters cross a trust boundary, such as when the Skill is wrapped by an API, automation service, bot, or job system that accepts user-controlled inputs. ### Attack Path 1. The Skill is exposed through automation or another interface that allows an untrusted party to influence `--webhook-url`. 2. The attacker supplies a URL targeting an internal service, a loopback listener, a private network host, a cloud metadata endpoint, or an attacker- ...[truncated 1261 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Parse the URL with `urllib.parse.urlsplit()` before constructing the request. - Require the `https` scheme. - Apply an explicit hostname allowlist for supported Discord webhook domains. - Require the expected `/api/webhooks/` path structure. - Reject URLs containing embedded usernames or passwords. - Reject unexpected ports. - Resolve the hostname and reject loopback, private, link-local, multicast, unspecified, and reserved addresses. - Disable automatic redirects or validate every redirect destination against the same policy. - Revalidate resolved addresses when connecting to reduce DNS rebinding risk. - If arbitrary relay endpoints are an intended feature, document that behavior explicitly and enforce a deployment-specific destination allowlist. - Ensure that callers do not permit untrusted users to supply request destinations unless such outbound access is an intentional, authorized capability. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/promo_scheduler.py:54
Finding
Non-Atomic Ledger Enforcement Permits Duplicate Daily Posts<![CDATA[ ## Vulnerability Details **File Location**: `scripts/promo_scheduler.py:54-64` and `scripts/ledger.py:101-121` **Vulnerability Type**: Race condition in anti-spam enforcement **Risk Level**: Medium ### Vulnerable Code The daily-post check and reservation are separate operations: ```python def cmd_post(args: argparse.Namespace) -> int: ledger = Ledger(args.ledger_path) today = today_local_yyyy_mm_dd() if ledger.already_posted_today(args.channel, today=today): eprint(f"Refusing to post: already posted today for channel={args.channel} date={today}") return 3 message = args.message msg_hash = sha256_hex(message) ledger.append(LedgerEntry(date=today, channel=args.channel, status="reserved", hash=msg_hash)) ``` The check only recognizes entries with a `posted` status: ```python def already_posted_today(self, channel: str, today: Optional[str] = None) -> bool: today_s = today or today_local_yyyy_mm_dd() with self._open_locked("a+") as f: f.seek(0) for line in f: line = line.strip() if not line: continue try: obj = json.loads(line) except Exception: continue if not isinstance(obj, dict): continue if obj.get("date") != today_s: continue if obj.get("channel") != channel: continue if obj.get("status") == "posted": return True return False ``` Lock acquisition failures are also silently ignored: ```python if fcntl is not None: try: fcntl.flock(f.fileno(), fcntl.LOCK_EX) except Exception: pass ``` ### Technical Analysis The intended invariant is a maximum of one successful post per day for each logical channel. File locking is applied separately inside `already_posted_today()` and `append()`, but the lock is released between the check and the reservation. ...[truncated 1812 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Implement a single atomic `check_and_reserve()` ledger operation. - Acquire an exclusive lock once, inspect the ledger, append the reservation, flush it, and release the lock only after the state transition is safely recorded. - Treat an active `reserved` entry for the same date and channel as blocking. - Add timestamps and reservation identifiers so abandoned reservations can be detected and recovered after a defined timeout. - Consider retaining the lock through the outbound request if blocking concurrent workers for the request duration is acceptable. - For stronger transactional guarantees, use SQLite with a unique constraint on `(date, channel)` and explicit transactions. - Do not suppress lock acquisition failures. Abort with a clear error if synchronization cannot be guaranteed. - Add concurrency tests that launch multiple processes simultaneously and verify that no more than one request is sent. - Define failure and retry semantics explicitly so a failed request can be retried without allowing simultaneous sends. ]]>
Vulnerability Patterns
  • 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
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (2)

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding
The skill description claims scheduling and anti-spam ledger protections, but the analysis indicates those controls may be absent from the actual implementation. When security-relevant safeguards are documented but not enforced, users may trust the skill to prevent repeated posting or provide auditability when it does not, increasing the risk of webhook abuse, spam, or unintended message delivery.

Lp3

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding
The skill advertises network and file-based behavior but does not declare any explicit tool scope or permissions boundaries. In an agent environment, this can lead to over-broad execution authority, making it easier for the skill to read local files or make outbound requests without clear user review or policy enforcement.