Back to skill

Security audit

Ghost Publisher

Security checks for vulnerabilities and agentic risk

Overview

This Ghost publishing skill does what it says, but it gives an agent live publishing, deletion, newsletter, network-fetch, and local-file upload power with weak safety boundaries.

Install only if you trust the calling agent and can point it at a staging or tightly controlled Ghost integration first. Use HTTPS Ghost URLs, restrict who can invoke publish/delete/newsletter actions, avoid passing untrusted image URLs or local paths, and run the skill with filesystem/network access limited to the content and media it genuinely needs.

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

T09 · Insecure Skill Coding Practices

Error
Location
publisher.py:538
Finding

Unrestricted Remote Image Retrieval Enables Server-Side Request Forgery

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
publisher.py:543
Finding

Arbitrary Local File Read and Upload Through the Image Interface

Content
View full analysis
max_bytes: raise RuntimeError( f"Image is {len(file_bytes)} bytes, exceeds configured max of " f"{max_bytes} bytes ({self.config.get('max_image_size_mb')}MB)." ) return _ghost_multipart_upload( self.ghost_url, self._jwt(), file_bytes=file_bytes, filename=filename, mime_type=mime_type, purpose="image", alt=alt, ) ``` ### Technical Analysis The declared interface legitimately supports uploading a local image path. However, the implementation treats every input that does not begin with `http://` or `https://` as an unrestricted filesystem path. There is no approved media-root boundary, canonical-path containment check, image extension allowlist, file-signature validation, symlink rejection, or regular-file check. `Path.read_bytes()` therefore reads any file accessible to the operating-system account running the Skill. The MIME type is inferred only from the supplied filename. A sensitive non-image file can be renamed, referenced through a misleading extension, or accessed through a symlink and then submitted as if it were an image. The configured size limit reduces the maximum amount disclosed in a single invocation but does not prevent sensitive-file disclosure. The bytes are uploaded to the configured Ghost media store. This behavior exceeds the minimum filesystem privilege required to upload user-selected images because it permits access to unrelated files anywhere readable by the process. ### Attack Path 1. An attacker influences an Agent or workflow to call `uploadImage`, `upload-image`, `set-image --upload`, or `c ...[truncated 1300 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Warning
Location
publisher.py:68
Finding

Ghost Administrative Credentials and Content May Be Transmitted Over Plaintext HTTP

Content
View full analysis
: from Ghost Admin > Integrations." ) return url, key ``` ```python def _ghost_get(ghost_url, jwt, path): url = f"{ghost_url}/ghost/api/admin/{path}" req = urllib.request.Request(url, headers={ "Authorization": f"Ghost {jwt}", "Accept": "application/json" }) with urllib.request.urlopen(req) as r: return json.loads(r.read()) def _ghost_send(ghost_url, jwt, path, body_dict, method="POST"): url = f"{ghost_url}/ghost/api/admin/{path}" body = json.dumps(body_dict).encode() req = urllib.request.Request(url, data=body, method=method, headers={ "Authorization": f"Ghost {jwt}", "Content-Type": "application/json", "Accept": "application/json" }) try: with urllib.request.urlopen(req) as r: data = r.read() return json.loads(data) if data else {} except urllib.error.HTTPError as e: body_err = e.read().decode(errors="replace") raise RuntimeError(f"Ghost API HTTP {e.code}: {body_err}") from e ``` ```python url = f"{ghost_url}/ghost/api/admin/images/upload/" req = urllib.request.Request(url, data=body, method="POST", headers={ "Authorization": f"Ghost {jwt}", "Content-Type": f"multipart/form-data; boundary ...[truncated 2277 chars]
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Behavioral ASTexec() Call, eval() Call, Dynamic Import
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
Findings (15)

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · publisher.py (reported line 306)May include surrounding context.

python
req = urllib.request.Request(url, headers={
        "Authorization": f"Ghost {jwt}", "Accept": "application/json"
    })
    with urllib.request.urlopen(req) as r:
        return json.loads(r.read())

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · publisher.py (reported line 319)May include surrounding context.

python
req = urllib.request.Request(url, headers={
        "Authorization": f"Ghost {jwt}", "Accept": "application/json"
    })
    with urllib.request.urlopen(req) as r:
        return json.loads(r.read())

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

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · publisher.py (reported line 364)May include surrounding context.

python
req = urllib.request.Request(url, headers={
        "Authorization": f"Ghost {jwt}", "Accept": "application/json"
    })
    with urllib.request.urlopen(req) as r:
        return json.loads(r.read())

Tp4

High
Category
MCP Tool Poisoning
Confidence
99% confidence
Finding

There is a clear mismatch between description and behavior. The description claims an operational Ghost CMS publisher with end-to-end publishing features. The actual code chunk is a non-networked test file whose primary purpose is validating interface conformance and constructor credential behavior. It does not itself publish articles, convert markdown, upload media, or interact with Ghost. This is not just an incomplete excerpt of one helper function; the supplied code's purpose is materially different from the declared skill purpose.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The declared description presents a production publishing skill for Ghost CMS with broad publishing and content-management capabilities. The supplied code chunk, however, is only a test module for one narrow internal function: converting markdown into Ghost Lexical JSON. It contains no Ghost API calls, no publishing workflow, no upload or scheduling logic, and no interface method implementations. This is a materially different primary purpose from the declared skill behavior, so it should be flagged as a mismatch.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
80% confidence
Finding

publishPost transitions content to a live public URL, and the documented send_newsletter option can notify subscribers, but the interface does not warn about these externally visible effects. In markdown descriptions, actions affecting user data, privacy, or system integrity should include clear disclosure.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
88% confidence
Finding

This markdown interface defines deletePost(id) as deleting a post, but provides no warning about data loss, confirmation requirement, or irreversible impact. Under the markdown-file criteria, destructive behavior that can affect user data should be accompanied by a warning in the skill description.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The interface explicitly allows adapters to fetch arbitrary remote URLs or read local file paths for image upload, which can enable SSRF against internal services or unintended local file access if untrusted input reaches this method. In the context of an agent-facing publishing skill, this is more dangerous because agents may pass through user-supplied URLs or paths without understanding the privacy and host-impact implications.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The README documents commands that can publish content, trigger newsletters, schedule releases, delete posts, and upload media, but it does not clearly warn users that these actions have real external side effects on a live Ghost instance. In an agent-skill context, missing safety guidance increases the risk of accidental destructive or high-impact actions being invoked against production sites, especially because newsletter sends and deletes may be irreversible or hard to recover from.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
91% confidence
Finding

The skill declares access to sensitive capabilities via its documented behavior and required environment variables, but it does not define any explicit tool scope such as allowed-tools or permissions. For a publishing skill that can read local files, use network access, and operate with an admin API key, missing scope boundaries increases the chance that an agent invokes it with broader-than-necessary privileges or without adequate operator awareness.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
92% confidence
Finding

The skill exposes a delete operation against a live CMS but does not prominently warn that deletion is destructive and may remove content irreversibly. In an agent-driven workflow, weak safety wording raises the risk of accidental destructive actions, especially because the skill is designed to operate directly on production Ghost sites using admin credentials.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
90% confidence
Finding

The uploadImage method will fetch arbitrary user-supplied http(s) URLs server-side and then upload the retrieved bytes to Ghost. In an agent/server environment, this can be abused as SSRF to reach internal services or cloud metadata endpoints, and the fetched content is then relayed onward, increasing the risk of internal data exposure; the publisher context makes this more dangerous because remote asset ingestion is an advertised workflow and likely exposed to untrusted inputs.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
84% confidence
Finding

The skill can publish with newsletter delivery to subscribers, but the description does not clearly warn that enabling this may trigger outbound email to real users and create privacy, reputational, and operational impact. In agent automation, omission of that warning makes unintended mass communication more likely.

Content

No source excerpt is available for this finding.

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · tests/test_interface.py (reported line 32)May include surrounding context.

python
cls = pub_mod.GhostPublisher
    for name in REQUIRED_METHODS:
        assert hasattr(cls, name), f"method {name} missing from GhostPublisher"
        assert callable(getattr(cls, name)), f"{name} is not callable"
    print(f"[PASS] all seven Publisher Interface v1 methods present: "
          f"{sorted(REQUIRED_METHODS)}")

Dynamic attribute access via getattr()

Low
Category
Dangerous Code Execution
Confidence
50% confidence
Finding

Dynamic getattr() with a non-literal attribute name can access arbitrary object attributes, potentially bypassing access controls.

Content

Scanner excerpt · tests/test_interface.py (reported line 40)May include surrounding context.

python
def test_method_signatures():
    cls = pub_mod.GhostPublisher
    for name, expected_params in REQUIRED_METHODS.items():
        sig = inspect.signature(getattr(cls, name))
        # Drop 'self'
        params = [p for p in sig.parameters if p != "self"]
        for p in expected_params:

Static analysis

No suspicious patterns detected.