Back to skill

Security audit

WordPress Remote News Publisher

Security checks for vulnerabilities and agentic risk

Overview

The skill's WordPress publishing purpose is clear, but its SSH automation can modify a live site and has concrete implementation risks that should be reviewed before install.

Treat this as a Review install. Use it only with a dedicated non-root SSH user and key, enable SSH host key verification, remove the --allow-root example, replace fixed /tmp state with private per-run files, validate numeric IDs, and fix remote command argument handling before enabling cron-based publishing.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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 (4)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/upload_media_remote.sh:24
Finding
SSH Host Authenticity Verification Is Disabled<![CDATA[ ## Vulnerability Details **File Location**: `scripts/upload_media_remote.sh:24-29`; also present in `scripts/publish_wp_remote.sh:19-24` and documented in `SKILL.md:120-127` **Vulnerability Type**: Improper SSH host verification **Risk Level**: Medium ### Complete Code Snippet ```bash SSH_OPTS="-i $SSH_KEY -p $SSH_PORT \ -o StrictHostKeyChecking=no \ -o BatchMode=yes \ -o ConnectTimeout=15 \ -o PasswordAuthentication=no" ``` The publishing script contains the equivalent configuration: ```bash SSH_OPTS="-i $SSH_KEY -p $SSH_PORT \ -o StrictHostKeyChecking=no \ -o BatchMode=yes \ -o ConnectTimeout=15 \ -o PasswordAuthentication=no" ``` ### Technical Analysis `StrictHostKeyChecking=no` causes SSH and SCP to accept an unknown host key automatically. Public-key authentication proves the client's identity to the server, but it does not protect the client unless the server's host key is independently verified. Consequently, DNS poisoning, routing manipulation, a compromised network gateway, or malicious modification of the configured hostname can redirect the Skill to an attacker-controlled SSH server. The scripts would then upload article and image data and execute the intended commands against that server without detecting the substitution. The private key contents are not directly transmitted by SSH, so this issue does not by itself disclose the private key. Nevertheless, it compromises server authenticity and the confidentiality and integrity of data exchanged by the workflow. The use of a dedicated private key is necessary for the declared remote-publishing function and does not inherently exceed minimum required privileges. The weakness is the absence of server verification, not the use of key-based authentication. ### Attack Path 1. The attacker gains the ability to redirect traffic for `WP_SSH_HOST`, such as through DNS poisoning, routing manipulation, or configu ...[truncated 908 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Provision the legitimate server's host key before running the Skill. - Replace `StrictHostKeyChecking=no` with: ```bash -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/secure/path/wp_known_hosts ``` - Generate the dedicated known-hosts file through a trusted administrative channel. Do not rely on an unauthenticated `ssh-keyscan` result without verifying its fingerprint. - Store SSH options in a Bash array so paths and values remain distinct arguments: ```bash SSH_OPTS=( -i "$SSH_KEY" -p "$SSH_PORT" -o StrictHostKeyChecking=yes -o UserKnownHostsFile="$WP_KNOWN_HOSTS" -o BatchMode=yes -o ConnectTimeout=15 -o PasswordAuthentication=no ) ``` - Continue using a dedicated, non-root SSH account and key with only the filesystem and WP-CLI permissions required for the target WordPress installation. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/publish_wp_remote.sh:100
Finding
Untrusted Values Are Interpolated into Remote Shell Commands<![CDATA[ ## Vulnerability Details **File Location**: `scripts/publish_wp_remote.sh:100-136` **Vulnerability Type**: Remote OS command injection **Risk Level**: High ### Complete Code Snippet ```bash # --- Set Featured Image --- echo "Setting featured image (Media ID: $MEDIA_ID)..." ssh $SSH_OPTS "$SSH_USER@$SSH_HOST" \ "wp post meta update $POST_ID _thumbnail_id $MEDIA_ID \ --path='$REMOTE_PATH'" 2>/dev/null || { echo "WARNING: Failed to set featured image" >&2 } # --- Add Tags --- if [ -n "$TAGS" ]; then echo "Adding tags: $TAGS" # Split comma-separated tags and add individually IFS=',' read -ra TAG_ARRAY <<< "$TAGS" for tag in "${TAG_ARRAY[@]}"; do ssh $SSH_OPTS "$SSH_USER@$SSH_HOST" \ "wp post term add $POST_ID post_tag '$tag' \ --path='$REMOTE_PATH'" 2>/dev/null || true done fi # --- Add Yoast SEO Metadata --- if [ -n "$META_DESC" ]; then echo "Adding Yoast meta description..." ssh $SSH_OPTS "$SSH_USER@$SSH_HOST" \ "wp post meta update $POST_ID _yoast_wpseo_metadesc '$META_DESC' \ --path='$REMOTE_PATH'" 2>/dev/null || { echo "WARNING: Failed to set Yoast meta description" >&2 } fi if [ -n "$KEYWORD" ]; then echo "Adding Yoast focus keyword: $KEYWORD" ssh $SSH_OPTS "$SSH_USER@$SSH_HOST" \ "wp post meta update $POST_ID _yoast_wpseo_focuskw '$KEYWORD' \ --path='$REMOTE_PATH'" 2>/dev/null || { echo "WARNING: Failed to set Yoast focus keyword" >&2 } fi ``` The author ID and remote path are also inserted into the remotely interpreted heredoc at `scripts/publish_wp_remote.sh:72-85`: ```bash POST_ID=$(ssh $SSH_OPTS "$SSH_USER@$SSH_HOST" bash <<ENDSSH python3 -c "import json; d=json.load(open('$REMOTE_ARTICLE_JSON')); print(d['title'], end='')" > /tmp/wp_title.txt python3 -c "import json; d=json.load(open('$REMOTE_ARTICLE_JSON')); print(d['content'], end='')" > /tmp/wp_content.txt python3 -c "i ...[truncated 2918 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Do not construct remote shell command strings from article fields. - Transfer structured JSON and invoke a fixed, reviewed remote wrapper. The wrapper should parse JSON and call WP-CLI through a process API that accepts an argument array rather than through a shell. - If a shell cannot be avoided, pass values as positional parameters and quote them inside the fixed remote script. Apply robust shell escaping such as `printf '%q'`; ordinary single-quote wrapping is insufficient. - Validate all identifier fields before use: ```bash [[ "$POST_ID" =~ ^[0-9]+$ ]] || exit 1 [[ "$MEDIA_ID" =~ ^[0-9]+$ ]] || exit 1 [[ "$AUTHOR_ID" =~ ^[0-9]+$ ]] || exit 1 ``` - Allowlist `WP_REMOTE_PATH` against an administrator-configured path rather than accepting arbitrary shell text. - Validate tag, keyword, and metadata lengths and reject control characters. Validation should supplement, not replace, safe argument handling. - Restrict the SSH account to a non-root user and a server-side wrapper that permits only the WP-CLI operations and WordPress path required by this Skill. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/publish_wp_remote.sh:33
Finding
Predictable Shared Temporary Files Permit Local Tampering and Symlink Attacks<![CDATA[ ## Vulnerability Details **File Location**: `scripts/publish_wp_remote.sh:33-53` **Vulnerability Type**: Insecure temporary-file handling **Risk Level**: High ### Complete Code Snippet ```bash # --- Read Article Data from JSON --- ARTICLE_JSON="/tmp/wp_article.json" if [ ! -f "$ARTICLE_JSON" ]; then echo "ERROR: Article JSON not found: $ARTICLE_JSON" >&2 echo "Generate article data first using the article generation phase." >&2 exit 1 fi if [ ! -f /tmp/wp_media_id.txt ]; then echo "ERROR: Media ID file not found: /tmp/wp_media_id.txt" >&2 echo "Upload cover image first." >&2 exit 1 fi # Extract article data using Python TITLE=$(python3 -c "import json; d=json.load(open('$ARTICLE_JSON')); print(d['title'])") CONTENT=$(python3 -c "import json; d=json.load(open('$ARTICLE_JSON')); print(d['content'])") EXCERPT=$(python3 -c "import json; d=json.load(open('$ARTICLE_JSON')); print(d['excerpt'])") TAGS=$(python3 -c "import json; d=json.load(open('$ARTICLE_JSON')); print(','.join(d['tags']))") META_DESC=$(python3 -c "import json; d=json.load(open('$ARTICLE_JSON')); print(d['meta_desc'])") KEYWORD=$(python3 -c "import json; d=json.load(open('$ARTICLE_JSON')); print(d['keyword'])") MEDIA_ID=$(cat /tmp/wp_media_id.txt) ``` Related predictable writes include `scripts/upload_media_remote.sh:88`: ```bash echo "$MEDIA_ID" > /tmp/wp_media_id.txt ``` The downloader also writes a fixed metadata path at `scripts/download_cover.py:63-68`: ```python meta_path = '/tmp/cover_meta.txt' with open(meta_path, 'w') as f: f.write(f"Photo by {photographer_name} on Unsplash\n") f.write(f"URL: {unsplash_url}\n") f.write(f"Downloaded: {datetime.utcnow().isoformat()}Z\n") ``` ### Technical Analysis The workflow stores article data, media IDs, post IDs, and image metadata under fixed names in the globally shared `/tmp` directory. It does not create a private per-run directory, verify ownership, reject symbolic links, or atomically open files ...[truncated 1844 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Create a private per-run directory and apply restrictive permissions: ```bash umask 077 WORK_DIR="$(mktemp -d)" trap 'rm -rf -- "$WORK_DIR"' EXIT ARTICLE_JSON="$WORK_DIR/wp_article.json" MEDIA_ID_FILE="$WORK_DIR/wp_media_id.txt" POST_ID_FILE="$WORK_DIR/wp_post_id.txt" ``` - Pass the private directory explicitly between workflow phases instead of relying on fixed global paths. - Verify file ownership and mode before consuming pre-existing state. - Reject symbolic links and use safe file-opening primitives with `O_NOFOLLOW`, `O_CREAT`, and `O_EXCL` where supported. - Write state to a temporary file in the same private directory and use an atomic rename after validation. - Validate media and post IDs as strictly numeric before using them. - Use unique remote temporary directories with restrictive permissions rather than shared names such as `/tmp/wp_title.txt`. ]]>

T08 · Insecure Dependencies

Note
Location
SKILL.md:277
Finding
Dependency Installation Guidance Is Unpinned and Non-Reproducible<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:277` **Vulnerability Type**: Unpinned third-party dependency **Risk Level**: Low ### Complete Code Snippet ```markdown | Python 3 | With `requests` library | `pip install requests` | ``` ### Technical Analysis The installation instruction asks users to install the latest version of `requests` and whatever transitive dependencies the package resolver selects at installation time. No reviewed version, lock file, or package hash is provided. The named package is legitimate, and the audited project does not contain evidence of dependency confusion, typosquatting, or intentional retrieval of a malicious package. The risk arises from non-reproducible dependency resolution and exposure to a future compromised or incompatible release. ### Attack Path 1. An operator follows the documented `pip install requests` instruction. 2. Pip queries the configured package index and resolves the current package and dependency versions. 3. A compromised package-index account, malicious index configuration, or compromised future dependency release supplies hostile package content. 4. Package installation executes applicable build or installation behavior under the operator's local privileges. 5. Malicious package code can subsequently execute when `download_cover.py` imports `requests`. ### Impact Assessment The impact is local code execution with the privileges of the user performing installation or running the Skill. If installation is performed as root or in a privileged global Python environment, system-wide files and other Python applications may also be affected. The practical likelihood is lower than the command-injection findings because no malicious package or unsafe custom index is present in the audited files. ]]>
Remediation
<![CDATA[ ## Remediation Suggestions - Define reviewed dependency versions in a requirements file. - Record cryptographic hashes and require hash verification: ```bash python3 -m pip install --require-hashes -r requirements.txt ``` - Install dependencies in a dedicated virtual environment rather than globally. - Use a trusted package index and explicitly review transitive dependencies. - Add automated dependency vulnerability scanning and a controlled update process so pinned versions can be upgraded after review. ]]>
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
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (19)

Tainted flow: 'headers' from os.environ.get (line 32, credential/environment) → requests.get (network output)

Critical
Category
Data Flow
Content
}
    
    try:
        response = requests.get(url, params=params, headers=headers, timeout=30)
        response.raise_for_status()
        data = response.json()
    except requests.exceptions.RequestException as e:
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Credential Access

High
Category
Privilege Escalation
Content
|----------|-------------|----------|---------|
| `WP_SSH_HOST` | Remote server hostname or IP | Yes | `203.0.113.10` or `example.com` |
| `WP_SSH_USER` | SSH username on remote server | Yes | `deploy`, `www-data`, `wpcli` |
| `WP_SSH_KEY` | Absolute path to SSH private key | Yes | `/home/user/.ssh/id_ed25519_wp` |
| `WP_SSH_PORT` | SSH port (default: 22) | No | `22` |
| `WP_REMOTE_PATH` | Absolute path to WordPress installation | Yes | `/var/www/html/wordpress` |
| `WP_REMOTE_TMP` | Writable temp directory on remote | No | `/tmp` |
Confidence
90% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
|----------|-------------|----------|---------|
| `WP_SSH_HOST` | Remote server hostname or IP | Yes | `203.0.113.10` or `example.com` |
| `WP_SSH_USER` | SSH username on remote server | Yes | `deploy`, `www-data`, `wpcli` |
| `WP_SSH_KEY` | Absolute path to SSH private key | Yes | `/home/user/.ssh/id_ed25519_wp` |
| `WP_SSH_PORT` | SSH port (default: 22) | No | `22` |
| `WP_REMOTE_PATH` | Absolute path to WordPress installation | Yes | `/var/www/html/wordpress` |
| `WP_REMOTE_TMP` | Writable temp directory on remote | No | `/tmp` |
Confidence
90% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
|----------|-------------|----------|---------|
| `WP_SSH_HOST` | Remote server hostname or IP | Yes | `203.0.113.10` or `example.com` |
| `WP_SSH_USER` | SSH username on remote server | Yes | `deploy`, `www-data`, `wpcli` |
| `WP_SSH_KEY` | Absolute path to SSH private key | Yes | `/home/user/.ssh/id_ed25519_wp` |
| `WP_SSH_PORT` | SSH port (default: 22) | No | `22` |
| `WP_REMOTE_PATH` | Absolute path to WordPress installation | Yes | `/var/www/html/wordpress` |
| `WP_REMOTE_TMP` | Writable temp directory on remote | No | `/tmp` |
Confidence
90% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
|----------|-------------|----------|---------|
| `WP_SSH_HOST` | Remote server hostname or IP | Yes | `203.0.113.10` or `example.com` |
| `WP_SSH_USER` | SSH username on remote server | Yes | `deploy`, `www-data`, `wpcli` |
| `WP_SSH_KEY` | Absolute path to SSH private key | Yes | `/home/user/.ssh/id_ed25519_wp` |
| `WP_SSH_PORT` | SSH port (default: 22) | No | `22` |
| `WP_REMOTE_PATH` | Absolute path to WordPress installation | Yes | `/var/www/html/wordpress` |
| `WP_REMOTE_TMP` | Writable temp directory on remote | No | `/tmp` |
Confidence
90% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
```bash
# 1. Generate a dedicated key pair (ed25519 recommended)
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_wp \
    -C "openclaw-wp-publisher" -N ""

# 2. Copy public key to remote server
Confidence
90% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
```bash
# 1. Generate a dedicated key pair (ed25519 recommended)
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_wp \
    -C "openclaw-wp-publisher" -N ""

# 2. Copy public key to remote server
Confidence
90% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
```bash
# 1. Generate a dedicated key pair (ed25519 recommended)
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_wp \
    -C "openclaw-wp-publisher" -N ""

# 2. Copy public key to remote server
Confidence
90% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
ssh -i ~/.ssh/id_ed25519_wp -o BatchMode=yes \
    deploy@203.0.113.10 "wp --info"

# 4. (Optional) Restrict key in ~/.ssh/authorized_keys
command="wp --allow-root",no-port-forwarding,no-X11-forwarding \
    ssh-ed25519 AAAA... openclaw-wp-publisher
```
Confidence
90% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Tool Parameter Abuse

High
Category
Tool Misuse
Content
deploy@203.0.113.10 "wp --info"

# 4. (Optional) Restrict key in ~/.ssh/authorized_keys
command="wp --allow-root",no-port-forwarding,no-X11-forwarding \
    ssh-ed25519 AAAA... openclaw-wp-publisher
```
Confidence
89% confidence
Finding
The authorized_keys restriction example uses `command="wp --allow-root"`, which permits forced execution of WP-CLI with root privileges if the associated key is used. In the context of a skill that automates remote content publishing and file transfer, granting a key a root-capable command materially increases the blast radius of key compromise or misuse and can enable full site or server modification depending on WP-CLI capabilities.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The skill is user-invocable and scheduled to run automatically, and it performs state-changing actions on a remote WordPress instance, including draft creation and publication. Although the behavior is described later in the document, the top-level description does not provide a clear warning that running the skill will modify a remote site on a schedule, increasing the risk of accidental or uninformed execution.

External Transmission

Medium
Category
Data Exfiltration
Content
sys.exit(1)
    
    # Request random photo from Unsplash
    url = 'https://api.unsplash.com/photos/random'
    params = {
        'query': query,
        'orientation': 'landscape',
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Tainted flow: 'image_url' from requests.get (line 50, network input) → requests.get (network output)

Medium
Category
Data Flow
Content
# Download the image
    try:
        image_url = data['urls']['regular']
        img_response = requests.get(image_url, timeout=60)
        img_response.raise_for_status()
        
        with open(output_path, 'wb') as f:
Confidence
83% confidence
Finding
The script performs a second network request to a URL taken directly from the first HTTP response without validating the hostname or scheme. If the upstream API response is malicious, compromised, or unexpectedly altered, this could turn into server-side request forgery behavior or allow downloads from unintended locations.

Intent-Code Divergence

Medium
Confidence
91% confidence
Finding
The documentation says the script creates a draft WordPress post from article data, which implies the parsed title, content, and excerpt are used as post fields. However, at L77-L80 the WP-CLI command supplies literal "file:///tmp/..." strings to --post_title, --post_content, and --post_excerpt, so the created post content does not match the documented intent of publishing the article body from JSON.

Unsafe Defaults

Medium
Category
Tool Misuse
Content
-o StrictHostKeyChecking=no \
          -o BatchMode=yes \
          -o ConnectTimeout=15 \
          -o PasswordAuthentication=no"

# --- Input Validation ---
if [ -z "$SSH_HOST" ] || [ -z "$SSH_USER" ] || [ -z "$SSH_KEY" ]; then
Confidence
98% confidence
Finding
The script disables SSH host key verification with StrictHostKeyChecking=no, which makes it vulnerable to man-in-the-middle attacks during both scp and ssh operations. Because the script transfers content and then executes remote WP-CLI commands, an attacker who can intercept or impersonate the SSH host could capture data, alter published content, or cause commands to run on an attacker-controlled server.

Unsafe Defaults

Medium
Category
Tool Misuse
Content
# -o StrictHostKeyChecking=no: don't prompt about host keys
# -o BatchMode=yes: fail if password required (force key auth)
# -o ConnectTimeout=15: timeout for connection
# -o PasswordAuthentication=no: explicitly disable password auth
SSH_OPTS="-i $SSH_KEY -p $SSH_PORT \
          -o StrictHostKeyChecking=no \
          -o BatchMode=yes \
Confidence
98% confidence
Finding
The SSH options explicitly disable host key verification with StrictHostKeyChecking=no, making the SCP and SSH connections vulnerable to man-in-the-middle attacks. An attacker on the network could impersonate the remote host, receive the uploaded file, or influence the remote command execution context while the script trusts the connection automatically.

Unsafe Defaults

Medium
Category
Tool Misuse
Content
-o StrictHostKeyChecking=no \
          -o BatchMode=yes \
          -o ConnectTimeout=15 \
          -o PasswordAuthentication=no"

# --- Input Validation ---
IMAGE_PATH="${1:-}"
Confidence
75% 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

Low
Confidence
88% confidence
Finding
This code creates or overwrites /tmp/wp_post_id.txt, which is a file write affecting local system state. While the script logs success afterward, there is no earlier user-facing warning, prompt, or comment specifically disclosing that it will persist the created post ID to a temporary local file.

Missing User Warnings

Low
Confidence
86% confidence
Finding
The script writes sensitive workflow state to a fixed, predictable path in /tmp, which is a world-accessible shared directory on many systems. This can expose the media ID to other local users, enable symlink or clobbering issues, and causes undocumented local filesystem side effects that may be abused in multi-user or automated environments.

Static analysis

No suspicious patterns detected.