Back to skill

Security audit

Publish-Mate

Security checks for vulnerabilities and agentic risk

Overview

This is a coherent news auto-publisher, but it needs Review because it can publish live CMS content while using unsafe TLS handling and broad URL fetching.

Before installing, require HTTPS with normal certificate validation, avoid HTTP CMS URLs, use a dedicated low-privilege WordPress application password, default first runs to preview or draft, and restrict news/image/custom API URLs to trusted public destinations.

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 (2)

T09 · Insecure Skill Coding Practices

Error
Location
scripts/auto_publish.py:506
Finding
WordPress credentials transmitted over connections with disabled TLS verification<![CDATA[ ## Vulnerability Details **File Location**: `scripts/auto_publish.py:506-511` **Vulnerability Type**: Improper certificate validation during authenticated CMS upload **Risk Level**: High ### Vulnerable Code ```python if use_ssl: ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE conn = httplib.HTTPSConnection(host, port, timeout=60, context=ctx) else: conn = httplib.HTTPConnection(host, port, timeout=60) ``` The resulting connection is used with headers constructed at `scripts/auto_publish.py:493-499`: ```python headers = { "Authorization": self.auth_header, "Content-Type": content_type, "Content-Disposition": f'attachment; filename="{filename}"', "User-Agent": "Mozilla/5.0 (compatible; OpenClaw-AutoPublisher/1.0)", "Connection": "close", "Content-Length": str(len(file_data)), } ``` ### Technical Analysis The image-upload implementation explicitly disables both certificate-chain validation and hostname verification for every HTTPS upload. Consequently, the client cannot verify that it is connected to the configured WordPress server. The request carries an HTTP Basic authorization header derived from the WordPress username and application password. Base64 encoding at `scripts/auto_publish.py:405-406` is the normal representation required by HTTP Basic authentication and is not encryption. Although the encoded credential is not printed to stdout, it is exposed if the upload connection is intercepted. The code also permits a CMS URL using plain HTTP, in which case the same credential is sent without transport encryption. Neither behavior is necessary for the declared publishing functionality and violates least-privilege credential handling. ### Attack Path 1. A user configures the Skill with a WordPress URL and supplies a valid application password through the environment. 2. The publishing pipeline prepares an image upload containing the Basic authorization ...[truncated 1319 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove the certificate-verification bypass and use a normal verified TLS context: ```python if use_ssl: ctx = ssl.create_default_context() conn = httplib.HTTPSConnection(host, port, timeout=60, context=ctx) else: raise ValueError("CMS publishing requires HTTPS") ``` 2. Reject non-HTTPS CMS URLs before loading or transmitting credentials. 3. If private certificate authorities must be supported, add an explicit configuration option for a trusted CA bundle rather than using `ssl.CERT_NONE`. 4. Do not provide a general-purpose insecure mode. If a development-only override is unavoidable, require explicit per-run consent, display a strong warning, and prohibit its use with real credentials. 5. Use a dedicated WordPress service account with only the capabilities required to create posts and upload media. 6. Rotate the application password after deploying the fix if the vulnerable upload path has been used on an untrusted network. 7. Apply the same HTTPS requirement and destination validation to `scripts/publish.py` and custom CMS publishing paths. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/fetch_news.py:72
Finding
Unrestricted URL fetching enables server-side request forgery<![CDATA[ ## Vulnerability Details **File Location**: `scripts/fetch_news.py:72-84` **Additional Locations**: `scripts/fetch_news.py:131-141`, `scripts/auto_publish.py:61-77`, `scripts/auto_publish.py:316-354`, `scripts/fetch_image.py:129-151`, `scripts/fetch_image.py:220-223` **Vulnerability Type**: Server-side request forgery through unvalidated feed, article, image, and command-line URLs **Risk Level**: Medium ### Vulnerable Code ```python def fetch_full_article(url: str, timeout: int = 15) -> str: """ Fetch full article content from URL using simple readability-like extraction. Returns cleaned text content. """ try: headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "text/html,application/xhtml+xml", } req = Request(url, headers=headers) with urlopen(req, timeout=timeout) as response: html = response.read().decode("utf-8", errors="ignore") ``` Feed-controlled article links reach this function at `scripts/fetch_news.py:220-225`: ```python if len(content) < 500 and link: # Try to fetch full article if content is too short logger.info(f"Fetching full article: {title[:50]}...") full_content = fetch_full_article(link) if full_content and len(full_content) > len(content): content = full_content ``` The same design is used for feed- and API-provided image URLs in the integrated pipeline: ```python rss_image = article.get("image_url", "") if rss_image and images_config.get("fallback_from_rss", True): logger.info(f"Using RSS image: {rss_image[:80]}...") local_path = download_image(rss_image, article["title"]) if local_path: return local_path ``` ### Technical Analysis URLs from configuration, RSS entries, API responses, redirects, and the `fetch_image.py --url` command-line option are passed directly to `urllib.request.urlopen`. The implementation does not: - Restrict requests ...[truncated 2847 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Introduce one centralized outbound URL validator and use it before every request, including redirects. 2. Permit only explicitly required schemes, preferably HTTPS. Reject `file`, `ftp`, `data`, and other non-web schemes. 3. Resolve the destination hostname and reject all loopback, private, link-local, reserved, multicast, and unspecified IPv4 and IPv6 ranges. 4. Explicitly block common cloud metadata destinations, including link-local metadata addresses. 5. Disable automatic redirects or validate every redirect target using the same policy before following it. 6. Reject URLs containing embedded usernames or passwords. 7. Where practical, allowlist official hosts for Unsplash, Pexels, Pixabay, NewsAPI, and configured public feed domains. 8. Apply strict response-size limits and stream downloads instead of calling `response.read()` without a bound. 9. Validate image response MIME types and file signatures before saving or uploading them. 10. Separate preview from publishing and require user confirmation before publishing content retrieved from a newly configured or untrusted source. 11. Run the Skill in an environment with outbound firewall rules that prevent access to internal networks and metadata services. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Rogue AgentSelf-Modification, Session Persistence
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (30)

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

Critical
Category
Data Flow
Content
}
    req = Request(url, headers=headers)
    try:
        with urlopen(req, timeout=timeout) as resp:
            return resp.read()
    except Exception:
        # Retry with SSL context
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

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

Critical
Category
Data Flow
Content
try:
            import certifi
            ctx = ssl.create_default_context(cafile=certifi.where())
            with urlopen(req, timeout=timeout, context=ctx) as resp:
                return resp.read()
        except ImportError:
            raise
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

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

Critical
Category
Data Flow
Content
}
    req = Request(url, headers=headers)
    try:
        with urlopen(req, timeout=15) as resp:
            data = json.loads(resp.read())
            results = data.get("results", [])
            if results:
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

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

Critical
Category
Data Flow
Content
}
    req = Request(url, headers=headers)
    try:
        with urlopen(req, timeout=15) as resp:
            data = json.loads(resp.read())
            results = data.get("results", [])
            if results:
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

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

Critical
Category
Data Flow
Content
"Authorization": self.auth_header,
                }
            )
            with urlopen(req, timeout=8) as resp:
                if resp.status == 200:
                    self.use_rest_route = False
                    logger.info("API: /wp-json/ style")
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

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

Critical
Category
Data Flow
Content
for attempt in range(retries + 1):
            try:
                req = Request(url, data=body, headers=headers, method=method)
                with urlopen(req, timeout=30) as resp:
                    return json.loads(resp.read().decode("utf-8"))
            except (URLError, ConnectionError, OSError) as e:
                if attempt < retries:
Confidence
90% confidence
Finding
Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Tp4

High
Category
MCP Tool Poisoning
Confidence
96% confidence
Finding
The declared description describes an end-to-end pipeline: fetching global news from RSS/API sources, automatically generating articles with images, and publishing them. The supplied code chunk implements only the publishing stage. It reads article data from a local JSON file, optionally does a dry run, uploads an existing image file, resolves/creates WordPress categories and tags, and publishes to WordPress or a configurable custom CMS endpoint. There is no logic for fetching news from RSS/API sources, scraping, summarization, article writing, or image generation. Therefore the description materially overstates what this code chunk actually does, making it a mismatch.

Context-Inappropriate Capability

High
Confidence
99% confidence
Finding
The image upload path explicitly disables TLS hostname verification and certificate validation before sending WordPress Basic Auth credentials and media content. This enables man-in-the-middle interception or credential theft on any hostile network path and is especially dangerous because the skill performs authenticated publishing to a CMS.

Missing User Warnings

High
Confidence
99% confidence
Finding
The code silently disables TLS verification for WordPress media uploads without any user warning or compensating control. Because Basic Auth credentials are used, an attacker intercepting traffic could capture credentials, alter uploaded content, or hijack the publishing workflow.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The README prominently markets automatic fetching, image generation, and publishing to a live WordPress or CMS target, but it does not clearly warn that invoking the skill may immediately create public posts and upload media to the user's site. In an automation context, that omission can cause unintended public content publication, reputational damage, and hard-to-reverse site changes, especially because users may assume a dry-run or draft-first workflow.

Missing User Warnings

Medium
Confidence
98% confidence
Finding
Documenting `publishing.status` with a default of `publish` normalizes immediate live publication without emphasizing that this makes generated content publicly visible right away. Because this skill automates both content generation and CMS posting, a risky default materially increases the chance of accidental publication of low-quality, incorrect, or policy-violating content.

Lp3

Medium
Category
MCP Least Privilege
Confidence
93% confidence
Finding
The skill declares no explicit tool scope even though its documented behavior requires environment access, file reads/writes, and outbound network actions. In an agent setting, missing scope declarations reduce transparency and guardrails, making it easier for the skill to perform sensitive actions like publishing content or reading secrets without clear user review.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The default command performs live network publishing to a user CMS, but the skill does not present a strong up-front warning or require confirmation before making external changes. This raises the risk of accidental content publication, unintended media uploads, and remote state changes triggered by a casual invocation of the main command.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The sample config sets `"language": "zh"`, which imposes a specific locale in the skill's natural-language behavior. The file does not explain that this is region-specific or present it as an explicit user choice in the example, creating a language policy concern.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
This is a markdown file, so SQP-2 applies to omissions in user-facing safety warnings. The section shows a plaintext `API_KEY` in `~/.openclaw/openclaw.json` but does not warn readers that credentials are sensitive, should be protected, and may be exposed via file sharing, backups, or source control.

Missing User Warnings

Medium
Confidence
86% confidence
Finding
The markdown describes automatic log analysis, alert webhooks, RSS/content extraction, image search, and WordPress auto-publishing, all of which can transmit operational or content data to external services. There is no accompanying warning about reviewing destinations, verifying consent/ownership of content, or the risk of exposing system or user data.

Session Persistence

Medium
Category
Rogue Agent
Content
1. 创建技能目录:
```bash
mkdir -p ~/.openclaw/workspace/skills/my-skill
```

2. 编写 `SKILL.md`:
Confidence
60% 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.

External Transmission

Medium
Category
Data Exfiltration
Content
"_comment": "Uncomment and configure for custom API",
      "type": "custom",
      "name": "My Custom Source",
      "url": "https://api.example.com/news",
      "headers": {
        "Authorization": "$MY_API_TOKEN"
      },
Confidence
50% 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
"_comment": "Uncomment and configure for custom API",
      "type": "custom",
      "name": "My Custom Source",
      "url": "https://api.example.com/news",
      "headers": {
        "Authorization": "$MY_API_TOKEN"
      },
Confidence
50% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Natural-Language Policy Violations

Medium
Confidence
95% confidence
Finding
The manifest sets `"language": "zh"` as a fixed default, which is a natural-language locale constraint. There is no indication in this file that users can opt into another language or that the skill is specifically intended only for a Chinese-language or China-region use case.

External Transmission

Medium
Category
Data Exfiltration
Content
def search_unsplash(query: str, api_key: str) -> str:
    """Search Unsplash for an image, return download URL."""
    url = f"https://api.unsplash.com/search/photos?query={quote(query)}&per_page=1&orientation=landscape"
    headers = {
        "Authorization": f"Client-ID {api_key}",
        "Accept": "application/json",
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: 'local_path' from os.environ.get (line 368, credential/environment) → open (file write)

Medium
Category
Data Flow
Content
if len(img_data) < 1000:
            logger.warning(f"Downloaded image too small ({len(img_data)} bytes), skipping")
            return ""
        with open(local_path, "wb") as f:
            f.write(img_data)
        logger.info(f"Image downloaded: {local_path} ({len(img_data)} bytes)")
        return str(local_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.

Natural-Language Policy Violations

Medium
Confidence
89% confidence
Finding
The pipeline assigns a default language of "en" when no language is configured, which imposes a specific locale without any visible user choice in this file. This can violate language/locale policy because the behavior prefers English by default rather than prompting for or documenting user opt-in.

External Transmission

Medium
Category
Data Exfiltration
Content
class PexelsFetcher:
    """Fetch images from Pexels API."""

    BASE_URL = "https://api.pexels.com/v1"

    def __init__(self, api_key: str):
        self.api_key = api_key
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
class PexelsFetcher:
    """Fetch images from Pexels API."""

    BASE_URL = "https://api.pexels.com/v1"

    def __init__(self, api_key: str):
        self.api_key = api_key
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Static analysis

No suspicious patterns detected.