Back to skill

Security audit

ghost cms

Security checks for vulnerabilities and agentic risk

Overview

This Ghost CMS skill does what it says, but it grants live blog admin powers and has under-scoped safeguards around credentials, publishing/deletion, API destinations, and remote image fetching.

Install only if you trust the publisher and need agent-assisted Ghost administration. Use a dedicated low-privilege Ghost integration if possible, keep the config file out of version control with restrictive permissions, require explicit human confirmation before publishing or deleting, use only HTTPS Ghost API URLs, and avoid passing untrusted remote feature-image URLs.

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

Error
Location
scripts/ghost.py:127
Finding
Arbitrary Remote Image Retrieval Enables SSRF and Internal Data Relay<![CDATA[ ## Vulnerability Details **File Location**: `scripts/ghost.py`, lines 127–163 **Vulnerability Type**: Server-Side Request Forgery (SSRF) and unrestricted data relay **Risk Level**: High ### Vulnerable Code ```python def download_image(url): """下载远程图片到临时文件""" try: print(f"⏳ 正在从远程下载图片: {url}") response = requests.get(url, timeout=10) response.raise_for_status() # 提取扩展名 import urllib.parse path = urllib.parse.urlparse(url).path ext = os.path.splitext(path)[1] or '.jpg' if ext.lower() not in ['.jpg', '.jpeg', '.png', '.gif', '.webp']: ext = '.jpg' temp_path = f"temp_download_{int(datetime.now().timestamp())}{ext}" with open(temp_path, 'wb') as f: f.write(response.content) return temp_path except Exception as e: print(f"❌ 下载远程图片失败: {e}") return None def create_post(config, title, content, status='draft', tags=None, excerpt=None, feature_image=None): """创建新文章 (自动检测并转换 Markdown)""" # 如果 feature_image 是远程 URL,尝试先下载并上传到 Ghost (确保稳定性) if feature_image and feature_image.startswith('http') and 'fu-ye.com' not in feature_image: print(f"📸 检测到外部图片 URL,正在尝试本地化上传...") local_path = download_image(feature_image) if local_path: ghost_image_url = upload_image(config, local_path) if ghost_image_url: feature_image = ghost_image_url # 清理临时文件 try: os.remove(local_path) except: pass ``` ### Technical Analysis The `feature_image` value is accepted as a remote URL and passed directly to `requests.get`. The implementation does not validate the URL scheme, destination hostname, resolved IP address, redirect destinations, response content type, or response size. The condition checking whether the text `fu-ye.com` occurs in the URL is not a security boundary. It does not establish that a destination is ...[truncated 1763 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Permit only explicitly supported URL schemes, preferably `https`. 2. Parse URLs with `urllib.parse.urlsplit` and reject malformed URLs, embedded credentials, and unexpected ports. 3. Resolve the hostname before connecting and reject loopback, private, link-local, reserved, multicast, and unspecified IPv4 and IPv6 addresses. 4. Disable redirects or validate every redirect destination using the same hostname and resolved-address policy. 5. Stream responses with a strict maximum byte count rather than loading the entire body into memory. 6. Verify that the response has an approved image media type and validate the actual file signature. 7. Consider restricting downloads to an explicit hostname allowlist. 8. Require explicit user confirmation before retrieving a remote feature image. 9. Do not automatically upload remotely retrieved data unless the retrieval and destination have both been validated. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/ghost.py:144
Finding
Predictable Temporary File Creation Allows Symlink-Based File Overwrite<![CDATA[ ## Vulnerability Details **File Location**: `scripts/ghost.py`, lines 144–146 **Vulnerability Type**: Insecure temporary-file creation **Risk Level**: Medium ### Vulnerable Code ```python temp_path = f"temp_download_{int(datetime.now().timestamp())}{ext}" with open(temp_path, 'wb') as f: f.write(response.content) return temp_path ``` ### Technical Analysis The temporary filename is derived from the current Unix timestamp and an attacker-influenced URL extension. It is therefore predictable and is created in the process's current working directory. Opening the path with `open(..., 'wb')` does not provide exclusive creation and follows symbolic links. A local attacker with write access to the working directory can pre-create the predicted filename as a symbolic link to another file. When the download occurs, Python follows that link and truncates or overwrites the target with attacker-selected remote content. Concurrent Skill executions within the same second and using the same extension can also select the same filename, causing data corruption or cross-operation interference. ### Attack Path 1. A local attacker determines the working directory and predicts the timestamp-based filename. 2. The attacker creates that path as a symbolic link to a file writable by the Skill process. 3. A remote feature-image download is triggered during the matching timestamp window. 4. The Skill opens the predictable path in write mode and follows the symbolic link. 5. The target file is truncated and replaced with the downloaded response. 6. If cleanup executes, `os.remove(local_path)` removes the symbolic link, potentially making the overwrite less apparent while leaving the target modified. ### Impact Assessment An attacker can overwrite or corrupt any file writable by the operating-system account running the Skill, provided the attacker can create entries in the working directory and win the timing race. The vulnerability does not independently grant perm ...[truncated 212 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Use `tempfile.NamedTemporaryFile(delete=False)` or `tempfile.mkstemp()` so that the file is created atomically with a random name and restrictive permissions. Do not construct temporary paths in the current working directory. Place cleanup in a `finally` block so it runs after both successful and failed uploads. Track whether the program created the file before deleting it, and avoid reopening temporary files through untrusted paths. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/ghost.py:38
Finding
Unvalidated API URL Can Transmit Ghost Administrative Tokens over Plaintext HTTP<![CDATA[ ## Vulnerability Details **File Location**: `scripts/ghost.py`, lines 38–105 **Vulnerability Type**: Plaintext transmission of privileged authentication tokens **Risk Level**: Medium ### Vulnerable Code ```python with open(target_file, 'r') as f: file_config = json.load(f) config = { 'api_url': file_config.get('api_url', ''), 'admin_api_key': file_config.get('admin_api_key', '') } if not config['api_url'] or not config['admin_api_key']: raise ValueError(f"❌ 配置文件 {target_file} 缺少 api_url 或 admin_api_key 字段") return config ``` ```python def get_headers(api_key, content_type='application/json'): """获取请求头""" token = generate_token(api_key) headers = { 'Authorization': f'Ghost {token}' } if content_type: headers['Content-Type'] = content_type return headers ``` ```python # 规范化 URL api_url = config['api_url'].rstrip('/') url = f"{api_url}/images/upload/" try: # 自动判断 MIME 类型 ext = os.path.splitext(image_path)[1].lower() mime_type = 'image/jpeg' if ext == '.png': mime_type = 'image/png' elif ext == '.gif': mime_type = 'image/gif' elif ext == '.webp': mime_type = 'image/webp' with open(image_path, 'rb') as f: files = {'file': (os.path.basename(image_path), f, mime_type)} headers = get_headers(config['admin_api_key'], None) print(f"⏳ 正在上传本地图片 {image_path} ({mime_type})...") response = requests.post(url, files=files, headers=headers) ``` The same authenticated-header construction is used by the create, update, delete, and list operations. ### Technical Analysis Configuration loading checks only that `api_url` and `admin_api_key` are nonempty. It does not require HTTPS or otherwise validate the API destination. If `api_url` uses `http://`, the generated Ghost administrative JWT is placed in the `Authorization` header and transmitted without transport encryption. Although the JWT expires after five mi ...[truncated 1332 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Parse `api_url` during configuration loading and require the `https` scheme. 2. Reject embedded credentials, fragments, malformed hostnames, and unsupported ports. 3. If local HTTP development is necessary, require an explicit insecure-development option and restrict it to verified loopback destinations. 4. Clearly display the validated destination before performing destructive or publishing operations. 5. Apply an expected-host allowlist where practical. 6. Continue using normal TLS certificate verification and do not introduce a `verify=False` bypass. 7. Recommend rotating the Admin API key immediately if a token or key may have crossed an untrusted plaintext network. ]]>

T08 · Insecure Dependencies

Note
Location
SKILL.md:77
Finding
Unpinned Runtime Dependencies Create Supply-Chain and Reproducibility Risk<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md`, lines 77, 153, and 306 **Vulnerability Type**: Unpinned third-party dependency installation **Risk Level**: Low ### Vulnerable Code ```bash pip3 install requests pyjwt --user ``` The same unpinned installation instruction appears more than once in the Skill documentation. Troubleshooting also instructs users to run: ```bash pip3 install pyjwt --user ``` ### Technical Analysis The documented installation commands resolve mutable latest versions of `requests` and `pyjwt` from whatever Python package index is configured in the user's environment. No reviewed versions, lock file, hash verification, or isolated environment is specified. This does not establish that either named package is malicious. However, it makes installations non-reproducible and exposes users to compromised future releases, a malicious configured package index, or incompatible dependency changes. Installing with `--user` also modifies the user's shared Python environment rather than isolating dependencies for this Skill. ### Attack Path 1. A user follows the documented `pip3 install` command. 2. Pip consults the environment's configured package indexes and resolves the current available releases. 3. If an index, package release, or transitive dependency has been compromised, unreviewed code is downloaded and installed. 4. Package installation hooks or later imports execute that code with the privileges of the user running the Skill. ### Impact Assessment A compromised dependency can execute arbitrary code with the privileges of the installing user. This could expose the Ghost configuration and Admin API key, modify user files, or affect other Python applications that share the user-level environment. Under normal trusted-index conditions, the more likely impact is unexpected incompatibility or changed behavior rather than compromise. ]]>
Remediation
<![CDATA[ ## Remediation Suggestions Pin reviewed versions of direct and transitive dependencies in a requirements or lock file. Generate and verify cryptographic hashes, for example through pip's `--require-hashes` workflow. Install the dependencies in a dedicated virtual environment rather than the shared user environment. Document the expected trusted package index and periodically update pins through a reviewed dependency-update process. ]]>
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
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (30)

Ae1

High
Category
analysis-evasion
Content
python3 scripts/ghost.py list --config "../../projects/fuye/ghost-admin.config.json"
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/ghost.py list --config "../../projects/fuye/ghost-admin.config.json"
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/ghost.py list --config "../../projects/fuye/ghost-admin.config.json"
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/ghost.py list --config "../../projects/fuye/ghost-admin.config.json"
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/ghost.py list --config "../../projects/fuye/ghost-admin.config.json"
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/ghost.py list --config "../../projects/fuye/ghost-admin.config.json"
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/ghost.py list --config "../../projects/fuye/ghost-admin.config.json"
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/ghost.py list --config "../../projects/fuye/ghost-admin.config.json"
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/ghost.py list --config "../../projects/fuye/ghost-admin.config.json"
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/ghost.py list --config "../../projects/fuye/ghost-admin.config.json"
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/ghost.py list --config "../../projects/fuye/ghost-admin.config.json"
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/ghost.py list --config "../../projects/fuye/ghost-admin.config.json"
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/ghost.py list --config "../../projects/fuye/ghost-admin.config.json"
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/ghost.py list --config "../../projects/fuye/ghost-admin.config.json"
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
python3 scripts/ghost.py list --config "../../projects/fuye/ghost-admin.config.json"
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Session Persistence

Medium
Category
Rogue Agent
Content
3. Click **"Add custom integration"**
4. Copy the **Admin API Key** (format: `id:secret`)

### 2. Create Configuration File

**唯一调用方式:自定义配置文件路径(项目隔离)**
Confidence
87% confidence
Finding
This finding is a duplicate detection of the same issue: the documentation directs users to store the Ghost Admin API key in a JSON config file under a stable project path. That persistence model exposes an administrative secret to unintended disclosure and subsequent unauthorized post creation, modification, or deletion if the file is accessed by another party or tool.

Session Persistence

Medium
Category
Rogue Agent
Content
3. Click **"Add custom integration"**
4. Copy the **Admin API Key** (format: `id:secret`)

### 2. Create Configuration File

**唯一调用方式:自定义配置文件路径(项目隔离)**
Confidence
87% confidence
Finding
This finding is a duplicate detection of the same issue: the documentation directs users to store the Ghost Admin API key in a JSON config file under a stable project path. That persistence model exposes an administrative secret to unintended disclosure and subsequent unauthorized post creation, modification, or deletion if the file is accessed by another party or tool.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The examples show immediate publishing with status set to published, but do not warn that content becomes publicly visible right away. In a content-management skill, this can lead to accidental disclosure of draft, sensitive, or unreviewed material through automation.

Missing User Warnings

Medium
Confidence
96% confidence
Finding
The skill documents deletion of Ghost posts without any warning, confirmation step, or guidance to verify the target post before execution. In an agentic context, this increases the chance of accidental irreversible content loss if a user request is ambiguous or the wrong post ID is supplied.

Context-Inappropriate Capability

Medium
Confidence
98% confidence
Finding
The documentation contains a real-looking Ghost Admin API key in an environment-variable example. Even if intended as a placeholder, publishing credential-shaped secrets normalizes unsafe secret handling and may result in users copying sensitive values into files or, if the token is real, immediate compromise of the Ghost admin interface.

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The documentation provides a delete command example with no warning, confirmation guidance, or mention of irreversibility. In an agent or automation context, this increases the chance of accidental destructive actions against live content, especially if users or downstream tools treat examples as safe defaults.

Natural-Language Policy Violations

Medium
Confidence
97% confidence
Finding
User-facing descriptions and runtime messages are consistently presented in Chinese, including the module description and CLI output, with no opt-in or alternative locale support. This creates a natural-language policy concern when a skill imposes a specific language on users without documented justification.

Intent-Code Divergence

Medium
Confidence
95% confidence
Finding
The docstring states the '唯一方式' is a custom config file path for project isolation, reinforced again in CLI help and errors. In practice, get_config simply expands and opens whatever path the caller supplies, with no restriction to a project directory or any isolation boundary, so the documentation overstates the safety/constraint actually enforced by code.

External Transmission

Medium
Category
Data Exfiltration
Content
headers = get_headers(config['admin_api_key'])
    
    try:
        response = requests.post(url, json=post_data, headers=headers)
        response.raise_for_status()
        data = response.json()
Confidence
80% 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
# PUT 请求需要带上 ID
        url = f"{api_url}/posts/{post_id}/?source=html"
        response = requests.put(url, json=update_data, headers=headers)
        response.raise_for_status()
        data = response.json()
Confidence
80% 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.