T09 · Insecure Skill Coding Practices
- Location
scripts/sync_to_ghost.py:73- Finding
Unrestricted RSS-Controlled Image Downloads Enable Server-Side Request Forgery
- Content
View full analysis
- Remediation
View remediation
Security audit
Security checks for vulnerabilities and agentic risk
The skill broadly matches its Mailchimp-to-Ghost purpose, but it needs Review because it uses Ghost admin access and downloads externally supplied image URLs without enough safety controls.
Install only if you are comfortable giving the skill a Ghost Admin API token and allowing it to fetch newsletter-controlled image URLs from the machine running the agent. Run it from the intended skill directory, avoid blindly copying the rm -rf cache/ commands, use reviewed pinned dependencies where possible, and review generated drafts before publishing or using any update/force behavior.
scripts/sync_to_ghost.py:73Unrestricted RSS-Controlled Image Downloads Enable Server-Side Request Forgery
SKILL.md:43Unpinned Third-Party Dependency Installation
The code substantially matches the 'fetch newsletter content from RSS' and 'convert HTML to clean markdown with horizontal rules' parts of the description. However, the declared purpose claims a full sync to Ghost, including image uploads and publishing as Lexical-format posts. This code chunk only performs feed retrieval, HTML cleanup, markdown conversion, and local file output. There is no Ghost authentication, API usage, image upload, or post publishing logic. The special 'button: ...' markers suggest downstream processing may exist elsewhere, but in this chunk the behavior is limited to markdown extraction and caching. Therefore the description overstates what this supplied code chunk actually does.
The code only implements the Ghost-side import portion of the described workflow. It operates on a local file (cache/newsletter_markdown_1.md) and contains no RSS retrieval, no Mailchimp integration, and no HTML-to-markdown conversion. It does perform image downloading/uploading and Markdown-to-Lexical conversion, including horizontal rules and other formatting. However, the declared description presents a fuller end-to-end sync pipeline and says it publishes posts, while this script creates drafts by default and optionally updates existing posts. These are material differences in primary behavior and capabilities, so this is a mismatch.
The skill instructs execution of rm -rf cache/ in a shell pipeline as an 'ALWAYS DO THIS FIRST' step. In an automated agent context, destructive shell parameters are dangerous because path assumptions can fail, aliases/symlinks can redirect deletion, and the command may run in an unintended directory, causing irreversible data loss.
# 1. Clear any stale cache and fetch fresh from RSS (ALWAYS DO THIS FIRST)
rm -rf cache/ && python3 scripts/fetch_markdown.py
# 2. Sync to Ghost (downloads images, uploads them, creates draft post)
python3 scripts/sync_to_ghost.py
This repeated recommendation to delete cache/ with rm -rf normalizes unsafe shell behavior without path validation. Repetition increases the likelihood that users or agents will execute it blindly, magnifying the risk of accidental deletion if the current directory is not what the instructions assume.
To force a fresh fetch, delete the cache:
rm -rf cache/
Using rm -rf cache/ in troubleshooting guidance remains dangerous because operators are more likely to copy-paste commands during failure scenarios without verifying context. In support/runbook text for an agent skill, this creates a realistic path to unintended destructive action.
Fix: Always clear cache and re-fetch before syncing:
rm -rf cache/
python3 scripts/fetch_markdown.py
python3 scripts/sync_to_ghost.py
This instruction again prescribes unguarded recursive deletion for routine maintenance. The skill context makes it more dangerous because the same document also relies on shell execution, file writes, and cached state, so an autonomous tool could plausibly run the command as written with broad filesystem access.
Delete cache and re-fetch:
rm -rf cache/
python3 scripts/fetch_markdown.py
The skill explicitly instructs use of Python scripts, shell commands, network access, and file deletion/writes, yet it declares no tool scope or permission boundaries. In an agent setting, missing scope metadata increases the chance the runtime grants broader capabilities than necessary, making destructive or unintended actions easier if the skill is invoked automatically.
The instructions tell users to place a Ghost admin token in a local env file but provide no warning that this credential grants sensitive administrative access. In a skill ecosystem, omission of credential-handling guidance increases the risk of token leakage through source control, logs, screenshots, or overbroad file access.
The skill recommends rm -rf cache/ as a routine step without emphasizing that it is a destructive command whose safety depends on the current working directory. In agentic or copy-paste workflows, destructive deletion instructions can cause accidental data loss if executed from the wrong path or after path manipulation.
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.
**Why this matters:** The RSS feed only shows the most recent newsletter(s). If you sync without re-fetching, you may:
- Create duplicate posts (Ghost will add `-2`, `-3` suffixes to slugs)
- Miss new newsletters entirely
- Process outdated content
The script’s documented behavior says it syncs markdown as a draft post, but the implementation can update existing Ghost posts when invoked with --update. This mismatch is dangerous because operators or higher-level agents may grant the skill more trust than warranted, leading to unintended modification of published or draft content in a production CMS.
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
try:
# Use NQL filter to search for posts with similar title
# The ~ operator does a partial match
result = subprocess.run(
[GHST_BIN, "post", "list", "--filter", f"title:~'{title[:30]}'", "--json"],
capture_output=True,
text=True,
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
def upload_image_to_ghost(image_path):
"""Upload an image to Ghost and return the new URL."""
try:
result = subprocess.run(
GHOST_IMAGE_UPLOAD_CMD + [image_path],
capture_output=True,
text=True,
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
try:
cmd.extend(["--lexical-file", lexical_file])
result = subprocess.run(
cmd,
capture_output=True,
text=True,
subprocess module calls execute external commands. Without careful input validation, this enables command injection.
try:
cmd.extend(["--lexical-file", lexical_file])
result = subprocess.run(
cmd,
capture_output=True,
text=True,
Skill's behavior or capabilities extend beyond its stated purpose. Scope creep allows an agent to perform actions unrelated to its documented functionality, increasing the attack surface.
permit persons to whom the Software is furnished to do so.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
The inline comment claims an '80% match threshold,' but the code actually returns a match when one normalized title is contained within the other. This is not merely incomplete documentation; it describes a different decision rule than the one implemented.
No suspicious patterns detected.