Back to skill

Security audit

Ghost-Writer Sync

Security checks for vulnerabilities and agentic risk

Overview

The skill performs the advertised blog-to-vault sync, but it under-discloses and mishandles sensitive Ghost Admin credentials.

Review before installing. Use only a dedicated, revocable Ghost integration key, avoid storing the config in synced or shared folders, do not run show_config where logs are captured, and only configure trusted HTTPS Ghost URLs. The package should ideally be updated to use least-privileged Ghost Content API credentials or clearly disclose Admin API use, redact secrets, restrict config permissions, and avoid shell-style command templates.

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

T09 · Insecure Skill Coding Practices

Error
Location
SKILL.md:18
Finding

Shell Command Injection Through Unescaped Tool Arguments

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
sync.py:398
Finding

Ghost Admin API Credentials Are Exposed Through Command Arguments, Plaintext Configuration, and Configuration Output

Content
View full analysis
None: """Save config to JSON file.""" Path(config_path).write_text( json.dumps(config, indent=2, ensure_ascii=False) + "\n", encoding="utf-8", ) ``` ```python if args.command == "config": print(json.dumps(cfg, indent=2)) ``` ```python elif args.command == "add-ghost": if not args.url or not args.api_key: print("Error: --url and --api-key are required for add-ghost", file=sys.stderr) sys.exit(1) cfg.setdefault("sources", {}).setdefault("ghost", []).append({ "url": args.url, "api_key": args.api_key, }) save_config(cfg, args.config) ``` The tool definition also places the key directly in a command argument: ```yaml execution: command: python3 {{SKILL_DIR}}/sync.py add-ghost --url "{{url}}" --api-key "{{api_key}}" --config "{{config}}" output_format: markdown ``` ### Technical Analysis The `add-ghost` command accepts the Ghost key through `--api-key`, stores it directly inside the JSON configuration, and writes that configuration without explicitly restricting filesystem permissions. The `config` command then serializes the entire configuration to standard output without redacting `api_key`. Consequently, the credential may be exposed through: - Shell history or command telemetry. - Process argument inspection while the command is running. - Skill execution logs. - The plaintext JSON configuration. - Backups or synchronization systems containing the configuration. - Output captured when the `config` tool is invoked. The credential format and implementation correspond to an Admin API secret used to derive bearer tokens, making disclosure more consequential than exposure of a public content key. ...[truncated 1242 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
sync.py:286
Finding

Admin Bearer Token Is Sent to an Unrestricted User-Configured URL

Content
View full analysis
list[dict]: """Fetch published posts from a Ghost CMS via Content API. Args: site_url: e.g. "https://myblog.ghost.io" api_key: Ghost Content API key ("id:secret" format) limit: max posts to return (default 50) """ token = _ghost_jwt_token(api_key) api_url = site_url.rstrip("/") + f"/ghost/api/admin/posts/?limit={limit}&order=published_at%20desc&formats=html" try: req = urllib.request.Request( api_url, headers={ "Authorization": f"Bearer {token}", "User-Agent": "Ghost-Writer-Sync/1.0", }, ) with urllib.request.urlopen(req, timeout=30) as resp: data = json.loads(resp.read().decode("utf-8")) except urllib.error.URLError as e: print(f"Error fetching Ghost posts: {e}", file=sys.stderr) return [] ``` The destination is saved without validation: ```python cfg.setdefault("sources", {}).setdefault("ghost", []).append({ "url": args.url, "api_key": args.api_key, }) ``` ### Technical Analysis Network access is necessary for the declared synchronization function. However, the code derives an Admin API bearer token and attaches it to a URL built directly from the user-configured `site_url`. There is no enforcement of HTTPS, no hostname allowlist or binding between the key and its legitimate Ghost host, and no rejection of loopback, link-local, private, or otherwise reserved network destinations. A plaintext `http://` source can therefore expose the token to network interception, while an attacker-controlled host can directly receive it. The unrestricted URL also creates an SSRF primitive. The Skill can ...[truncated 1817 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
SKILL.md:41
Finding

Documentation Misrepresents Admin API Authentication as Content API Authentication

Content
View full analysis
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • 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
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (4)

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding

The skill exposes network access plus local file read/write behavior but does not declare any explicit tool scope or permission boundaries. That omission increases the risk of over-broad execution in host environments, making it harder for operators and automated policy systems to constrain where it can read, write, or connect.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The helper docstring says it builds a 'Ghost Admin API JWT' and that the 'Ghost Content API accepts these tokens,' while the rest of the code uses an admin endpoint ('/ghost/api/admin/posts/') and the CLI/config documentation labels the credential as a generic 'Ghost API key'. This is an intent-level contradiction because the file header claims Ghost support 'via Content API with Admin API key' (L006), but the implementation actually uses the Admin API and an Admin API token format.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The skill stores Ghost API keys in plaintext JSON on disk via save_config(), with no warning, permission hardening, or safer secret-handling option. If the config file is readable by other local users, synced to cloud storage, committed to version control, or harvested by other tools in the vault environment, the Ghost Admin credential can be exposed and used to access or modify blog content via the Ghost Admin API.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
88% confidence
Finding

The tool description for add_ghost says it requires a Ghost Content API key, which is consistent with a read-only sync tool. However, the Supported Sources table states 'Uses Admin API JWT auth', which implies a different and more privileged API capability. This is an internal documentation contradiction about the skill's intended access level.

Content

No source excerpt is available for this finding.