Back to skill

Security audit

Multi Platform Crosspost

Security checks for vulnerabilities and agentic risk

Overview

This skill broadly matches a blog cross-posting purpose, but it needs review because it can publish publicly and has unsafe webhook authentication, external content-sharing, and capability-disclosure gaps.

Install only if you are comfortable giving the workflow posting and email authority. Before enabling it with real credentials, move webhook authentication before any blog-admin or external requests, use stronger signed webhook authentication, validate slug/lang inputs, add a review step before public posting, make OpenAI processing explicit opt-in, sanitize email HTML, and align the documented platform list with what the workflows actually do.

Vulnerability Patterns
  • Skill Instruction HijackingAlters the agent's session goals or safety constraints when the skill loads
  • Unauthorized Access and Privilege EscalationObtains permissions beyond the task's legitimate needs
  • Insecure Skill Coding PracticesFinds exploitable flaws such as hardcoded secrets or command injection
  • 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 (6)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
workflows/aethon-blog-crosspost.json:25
Finding
Webhook Authentication Occurs After a Privileged Internal API Request<![CDATA[ ## Vulnerability Details **File Location**: `workflows/aethon-blog-crosspost.json`, lines 25-48 **Vulnerability Type**: Authentication-order flaw and unauthorized use of an internal service credential **Risk Level**: High ### Complete Code Snippet ```json { "parameters": { "url": "=http://YOUR_BLOG_ADMIN_HOST:3000/api/posts/{{ $json.body.slug }}?lang={{ $json.body.lang || 'en' }}", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "X-API-Key", "value": "YOUR_BLOG_ADMIN_API_KEY" } ] }, "options": {} }, "id": "n2", "name": "Fetch Post", "type": "n8n-nodes-base.httpRequest" } ``` The webhook secret is validated only in the subsequent node: ```js const webhook = $('Webhook').first().json; const post = $input.first().json; // C4: Verify webhook authentication via body secret const secret = webhook.body?._secret || webhook._secret || ''; const expectedSecret = 'YOUR_CROSSPOST_SECRET'; if (expectedSecret && secret !== expectedSecret) { throw new Error('Unauthorized: invalid crosspost secret'); } ``` ### Technical Analysis The workflow performs an API-key-authenticated request to the internal blog administration service before authenticating the webhook caller. Consequently, possession of the webhook URL is sufficient to make the n8n service invoke the internal endpoint using `YOUR_BLOG_ADMIN_API_KEY`. The caller-controlled `slug` and `lang` values are also interpolated directly into the URL without an allowlist or explicit URL encoding. Depending on the blog service's router and proxy behavior, crafted values may alter the path or query string. Even when the final authentication check rejects the request, the privileged internal request has already occurred. This exceeds least privilege because an unauthenticated external caller can exercise a network capability and credential that should only be available to authenticated cross-post operations. ### At ...[truncated 1201 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Move webhook authentication into the first executable node after the webhook, before all HTTP, Google Sheets, email, Slack, or platform operations. 2. Prefer a secret in an HTTP header such as `Authorization` or `X-Crosspost-Secret` rather than in the request body. 3. Compare secrets using a constant-time comparison where supported. 4. Reject the request immediately if the production secret is missing or still set to a placeholder. 5. Restrict `slug` to an expected pattern, such as `^[a-z0-9]+(?:-[a-z0-9]+)*$`. 6. Restrict `lang` to an explicit allowlist such as `en`, `de`, or other configured languages. 7. Use structured query parameters and URL encoding rather than raw string interpolation. 8. Apply webhook rate limiting, request-size limits, and access logging. 9. Give the blog-admin credential read-only access limited to the exact post endpoint. ]]>

T05 · Unauthorized Access and Privilege Escalation

Error
Location
workflows/ink-blog-crosspost.json:25
Finding
Webhook Authentication Occurs After a Privileged Internal API Request<![CDATA[ ## Vulnerability Details **File Location**: `workflows/ink-blog-crosspost.json`, lines 25-48 **Vulnerability Type**: Authentication-order flaw and unauthorized use of an internal service credential **Risk Level**: High ### Complete Code Snippet ```json { "parameters": { "url": "=http://YOUR_BLOG_ADMIN_HOST:3000/api/posts/{{ $json.body.slug }}?lang={{ $json.body.lang || 'en' }}", "sendHeaders": true, "headerParameters": { "parameters": [ { "name": "X-API-Key", "value": "YOUR_BLOG_ADMIN_API_KEY" } ] }, "options": {} }, "id": "a2", "name": "Fetch Post", "type": "n8n-nodes-base.httpRequest" } ``` Authentication is performed only after that request: ```js const webhook = $('Webhook').first().json; const post = $input.first().json; // Verify webhook authentication via body secret const secret = webhook.body?._secret || webhook._secret || ''; const expectedSecret = 'YOUR_CROSSPOST_SECRET'; if (expectedSecret && secret !== expectedSecret) { throw new Error('Unauthorized: invalid crosspost secret'); } ``` ### Technical Analysis The workflow trusts caller-provided `slug` and `lang` values sufficiently to make an internal request carrying `YOUR_BLOG_ADMIN_API_KEY` before it establishes that the caller is authorized. Rejecting the request afterward cannot undo the internal request. The URL uses raw n8n expression interpolation rather than validated and encoded path/query values. This creates additional risk if the downstream router accepts unexpected path delimiters or query syntax. ### Attack Path 1. An attacker submits a request to `/webhook/blog-crosspost`. 2. The request contains an invalid or missing `_secret` and attacker-selected `slug` or `lang`. 3. n8n invokes the internal blog-admin endpoint with its privileged API key. 4. The internal service processes the request. 5. The later code node detects the invalid secret and terminates the remaining workflow. 6. Repeated ...[truncated 547 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Validate the webhook secret immediately after the webhook and before `Fetch Post`. 2. Fail closed when the expected secret is empty, missing, or still a placeholder. 3. Move authentication to a request header and avoid secrets in request bodies and execution data. 4. Validate `slug` and `lang` against strict allowlists. 5. URL-encode all path and query values or use structured request-node query parameters. 6. Rate-limit the webhook and monitor failed authentication attempts. 7. Scope `YOUR_BLOG_ADMIN_API_KEY` to read-only access for the required endpoint. 8. If network architecture permits, restrict the blog-admin endpoint so it accepts calls only from the n8n service identity. ]]>

T01 · Skill Instruction Hijacking

Error
Location
workflows/aethon-blog-crosspost.json:119
Finding
Untrusted Article Content Can Influence Automatically Published LLM Output<![CDATA[ ## Vulnerability Details **File Location**: `workflows/aethon-blog-crosspost.json`, lines 119-329 **Vulnerability Type**: Indirect prompt injection into an automated publishing pipeline **Risk Level**: High ### Complete Code Snippet The article is inserted directly into the model prompt: ```js const userPrompt = `Generate high-impact social media content for this YOUR_COMPANY_NAME blog post: TITLE: ${post.title} DESCRIPTION: ${post.description} URL: ${post.canonical_url} TAGS: ${(post.tags || []).join(', ')} CATEGORIES: ${(post.categories || []).join(', ')} ARTICLE CONTENT: ${post.body_truncated} === PLATFORM REQUIREMENTS === ... `; const requestBody = { model: 'gpt-4o-mini', messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt } ], response_format: { type: 'json_object' }, temperature: 0.75, max_tokens: 14000 }; ``` The response is parsed without content-policy validation: ```js let ai; try { const raw = openaiResp.choices[0].message.content; ai = JSON.parse(raw); } catch(e) { // Fallback generation omitted here because it is not the vulnerable model-output path. } ``` Model-generated values are then prepared for publication: ```js const devtoBody = JSON.stringify({ article: { title: ai.devto?.title || prev.title, body_markdown: (ai.devto?.body || prev.body) + '\n\n---\n\n*Originally published on [YOUR_BLOG_NAME](' + prev.canonical_url + ')*', canonical_url: prev.canonical_url, tags: (ai.devto?.tags || prev.tags).slice(0,4).map(t => String(t).toLowerCase().replace(/[^a-z0-9]/g,'')), published: true } }); return [{ json: { ...prev, ai, linkedin_text: ai.linkedin?.text || (prev.title + '\n\n' + prev.description + '\n\n#YourBrandHashtag'), devto_body: devtoBody, hashnode_body: hashnodeBody } }]; ``` The values are sent to publishing nodes: ```json { "person": "YOUR_LINKEDIN_PERSON_ID", "text": "={{ $json.linkedin_text }}\n\n{{ ...[truncated 2382 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Require human review and explicit approval before any LLM-generated content is publicly posted. 2. Treat article content as untrusted data. Place it inside strong delimiters and explicitly instruct the model that text inside those delimiters must never be treated as instructions. 3. Use a separate extraction stage to derive factual content before generating platform posts. 4. Validate the model response against a strict JSON schema with field lengths, types, and allowed tag formats. 5. Reject external URLs unless they match an explicit allowlist, such as the configured canonical blog domain. 6. Scan output for prompt leakage, credential requests, prohibited claims, unexpected calls to action, and unsafe URL schemes. 7. Set generated articles to drafts where supported instead of using `published: true`. 8. Keep automatic platform scopes narrowly limited and separate draft creation credentials from publication credentials where APIs permit. 9. Record the source text, model response, validation result, and approving user for auditability. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
workflows/aethon-blog-crosspost.json:600
Finding
Raw Article and Model Output Are Embedded in HTML Email Without Sanitization<![CDATA[ ## Vulnerability Details **File Location**: `workflows/aethon-blog-crosspost.json`, lines 600-603 **Vulnerability Type**: HTML injection in generated notification email **Risk Level**: Medium ### Complete Code Snippet ```html <div style="background: #fafafa; padding: 16px; border: 1px dashed #ccc; border-radius: 8px; margin: 8px 0 20px; max-height: 400px; overflow-y: auto;"> <div style="font-family: Georgia, serif; line-height: 1.8; font-size: 15px;">{{ ($json.ai?.substack?.body || $json.body || '').replace(/\n/g, '<br>') }}</div> </div> ``` Other dynamic model fields are also inserted into the HTML template: ```html <pre style="white-space: pre-wrap; font-family: -apple-system, sans-serif; font-size: 14px; margin: 0;">{{ $json.ai?.facebook?.text || 'Content generation failed' }} {{ $json.canonical_url }}</pre> ``` ```html <pre style="white-space: pre-wrap; font-family: -apple-system, sans-serif; font-size: 14px; margin: 0;">{{ $json.ai?.reddit?.body || $json.description + '\n\nFull article: ' + $json.canonical_url }}</pre> ``` ### Technical Analysis Replacing newline characters with `<br>` does not sanitize HTML. If the article body or model response contains HTML tags, those tags become part of the email body. Email clients commonly block scripts, but dangerous and deceptive markup remains possible, including tracking pixels, externally loaded images, misleading links, hidden text, layout manipulation, and client-dependent active content. The model output is additionally influenced by untrusted article content, expanding the injection path. ### Attack Path 1. An attacker places crafted HTML in a source article or causes the LLM to reproduce crafted markup. 2. The cross-post workflow stores that content in `$json.body` or `$json.ai`. 3. The `Send Platform Content` node interpolates the value directly into its HTML template. 4. The configured SMTP service sends the resulting HTML message. 5. A notification recipient opens the message. 6. T ...[truncated 536 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. HTML-escape every dynamic value before inserting it into the email template. 2. If limited rich text is required, sanitize it with a strict allowlist that permits only necessary formatting tags and attributes. 3. Remove image tags, forms, style blocks, event attributes, embedded objects, and unsafe URL schemes. 4. Validate links so only `https` URLs on expected domains are rendered. 5. Prefer a plain-text MIME part for raw article content. 6. Do not assume `<pre>` prevents HTML interpretation; escaped content must be used inside all HTML elements. 7. Add test cases containing tags such as `<img>`, `<a>`, malformed markup, and encoded HTML to verify sanitization. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
workflows/ink-blog-crosspost.json:272
Finding
Raw Blog Content Is Embedded in Substack HTML Email Without Sanitization<![CDATA[ ## Vulnerability Details **File Location**: `workflows/ink-blog-crosspost.json`, lines 272-275 **Vulnerability Type**: HTML injection in generated notification email **Risk Level**: Medium ### Complete Code Snippet ```json { "fromEmail": "YOUR_FROM_EMAIL", "toEmail": "YOUR_NOTIFICATION_EMAIL", "subject": "=[Substack Ready] {{ $('Determine Platforms').first().json.title }}", "html": "=<h2>Ready to paste into Substack</h2>\n<p><strong>Title:</strong> {{ $('Determine Platforms').first().json.title }}</p>\n<p><strong>Canonical URL:</strong> <a href=\"{{ $('Determine Platforms').first().json.canonical_url }}\">{{ $('Determine Platforms').first().json.canonical_url }}</a></p>\n<hr>\n<div style=\"font-family: Georgia, serif; max-width: 680px; line-height: 1.8;\">{{ $('Determine Platforms').first().json.body.replace(/\\n/g, '<br>') }}</div>\n<hr>\n<p><em>Copy the content above and paste it into your Substack editor. Set the canonical URL to: {{ $('Determine Platforms').first().json.canonical_url }}</em></p>", "options": {} } ``` ### Technical Analysis The complete blog body is inserted into an HTML email after only replacing newline characters. No HTML escaping or sanitization is performed. A source article containing HTML can therefore alter the notification email's markup. Although modern email clients generally suppress JavaScript, the injected content may include remote images, tracking resources, misleading links, hidden content, or layout changes. ### Attack Path 1. An attacker creates or modifies a blog post containing crafted HTML. 2. The post is fetched by the workflow. 3. The attacker-controlled body reaches the `Email for Substack` node. 4. Newlines are changed to `<br>`, but existing markup remains intact. 5. The SMTP service sends the resulting message to `YOUR_NOTIFICATION_EMAIL`. 6. The recipient's email client renders the injected markup. ### Impact Assessment The vulnerability affects recipients of the configured Substack-re ...[truncated 210 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Escape the blog body before inserting it into the HTML email. 2. Use a well-maintained HTML sanitizer with a minimal allowlist if preserving formatting is necessary. 3. Strip external images, unsafe links, event attributes, forms, embedded content, and styling that can obscure notification text. 4. Include the original content as an escaped plain-text section or attachment rather than executable HTML markup. 5. Validate the canonical URL and other interpolated attributes before placing them in `href` values. 6. Test the template against malicious and malformed HTML payloads. ]]>

other

Warning
Location
workflows/aethon-blog-crosspost.json:119
Finding
OpenAI Processing Is Described as Optional but Article Content Is Sent Unconditionally<![CDATA[ ## Vulnerability Details **File Location**: `workflows/aethon-blog-crosspost.json`, lines 119-159 **Vulnerability Type**: Unconditional external disclosure of article content **Risk Level**: Medium ### Complete Code Snippet The article excerpt is placed into the OpenAI request: ```js const userPrompt = `Generate high-impact social media content for this YOUR_COMPANY_NAME blog post: TITLE: ${post.title} DESCRIPTION: ${post.description} URL: ${post.canonical_url} TAGS: ${(post.tags || []).join(', ')} CATEGORIES: ${(post.categories || []).join(', ')} ARTICLE CONTENT: ${post.body_truncated} === PLATFORM REQUIREMENTS === ... `; const requestBody = { model: 'gpt-4o-mini', messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt } ], response_format: { type: 'json_object' }, temperature: 0.75, max_tokens: 14000 }; return [{ json: { ...post, openai_request: requestBody } }]; ``` The request is then sent to OpenAI without an enablement condition: ```json { "parameters": { "method": "POST", "url": "https://api.openai.com/v1/chat/completions", "authentication": "predefinedCredentialType", "nodeCredentialType": "openAiApi", "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify($json.openai_request) }}", "options": { "timeout": 120000 } }, "id": "n8", "name": "Call OpenAI" } ``` The source documentation states that OpenAI is optional: ```markdown | OpenAI (optional) | AI-powered content formatting | `YOUR_OPENAI_CREDENTIAL_ID` | ``` ### Technical Analysis The workflow truncates the full body to 5,000 characters for the prompt, but it does not check an `enable_openai` option, selected platforms, publication state beyond the draft check, or a content-sensitivity flag before sending the request. This conflicts with the documented characterization of OpenAI as optional. An operator may reasonably expect that omitting AI-b ...[truncated 1394 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Add an explicit `enable_openai` configuration option that defaults to `false`. 2. Route around both `Build AI Prompt` and `Call OpenAI` when AI processing is disabled. 3. Require explicit per-request approval before sending unpublished, confidential, or client-owned content. 4. Document precisely which article fields are transferred, the maximum body length, the destination, and applicable retention or processing policies. 5. Minimize transmitted data by sending only the fields needed for the selected platform. 6. Apply redaction or data-loss-prevention checks before external transfer. 7. Provide deterministic local formatting as the default fallback. 8. Restrict the OpenAI credential to this workflow and monitor request volumes for unexpected use. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (10)

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The skill explicitly automates distribution of full blog content to multiple third-party platforms and also emails formatted article content, but the introductory description does not prominently warn users about that external publication and data transmission. This can lead users to trigger the workflow without fully understanding that content, metadata, and possibly unpublished or sensitive material fetched from the blog admin API will be sent outside their primary environment.

External Transmission

Medium
Category
Data Exfiltration
Content
### 4. Trigger
```bash
# Via webhook
curl -X POST https://your-n8n.com/webhook/blog-crosspost \
  -H "Content-Type: application/json" \
  -d '{"slug": "my-article", "lang": "en", "platforms": "all", "_secret": "your-secret"}'
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Description-Behavior Mismatch

Medium
Confidence
98% confidence
Finding
The manifest describes an 'Automatically cross-post' pipeline for 7+ named platforms including Twitter/X, Reddit, Substack, and Pinterest. In the workflow response and downstream logic, only LinkedIn, Dev.to, and Hashnode are actually auto-posted, while Twitter, Facebook, Reddit, and Substack are marked manual/content-ready, and there is no Pinterest handling anywhere in the file.

Ssd 3

Medium
Confidence
92% confidence
Finding
The workflow sends article content to OpenAI and later distributes generated and source-derived content through Slack and email, while also carrying full body text and AI outputs through workflow state. If the source post contains unpublished, sensitive, regulated, or inadvertently embedded secrets, the pipeline broadens exposure across multiple third-party services and internal channels.

External Transmission

Medium
Category
Data Exfiltration
Content
{
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/chat/completions",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "openAiApi",
        "sendBody": true,
Confidence
50% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The workflow performs real side effects—posting to external platforms and sending Slack/email updates—after only a webhook-triggered secret check, with no explicit approval gate or human confirmation before publication. In a publishing automation context, this creates a meaningful risk of accidental or unauthorized distribution if the webhook secret is leaked, guessed, mishandled, or invoked by an internal system unexpectedly.

Description-Behavior Mismatch

Medium
Confidence
98% confidence
Finding
The manifest describes a production-tested pipeline that automatically cross-posts to seven or more named platforms. In this file, the only real outbound publishing action is a LinkedIn post, and Substack is handled by sending an email for manual paste; the other named platforms are not implemented here despite status fields being recorded for some of them.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
This JSON workflow posts content to LinkedIn and elsewhere in the flow sends full post bodies by email, but the file contains no user-facing disclosure, confirmation step, or explanatory comment warning that content will be transmitted to third-party services. Because this is a manifest-style workflow file rather than README documentation, there is no visible in-file warning to alert operators before cross-posting and emailing article content.

Description-Behavior Mismatch

Low
Confidence
87% confidence
Finding
The manifest enumerates LinkedIn, Dev.to, Hashnode, Twitter/X, Reddit, Substack, and Pinterest. The AI prompt and later result handling include Facebook content generation and operational reporting, which expands platform scope beyond the declared description.

Intent-Code Divergence

Low
Confidence
92% confidence
Finding
The workflow accepts a shared secret from the request body as its main authentication control for a webhook that triggers outbound posting and email actions. Body-based secrets are weaker than standard webhook authentication schemes because they are easier to mishandle, log, replay, or leak through upstream systems, and there is no signature, timestamp, or source verification shown here.