Back to skill

Security audit

Email Outreach Automation

Security checks for vulnerabilities and agentic risk

Overview

This is a coherent self-hosted outreach automation skill, but its public webhooks and automated email workflows need review before use because they can modify prospect records and send notifications without enough safeguards.

Review before installing. Use only with lawful, consent-aware prospect lists; configure a strong OUTREACH_SECRET and fail closed if it is missing; authenticate or signature-verify reply webhooks; add rate limits and request-size limits; use suppression and opt-out handling; restrict Google Sheets and SMTP credentials to the minimum needed; validate admin report recipients; and avoid rendering raw reply content as HTML.

Vulnerability Patterns
  • 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
  • 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 (3)

T05 · Unauthorized Access and Privilege Escalation

Error
Location
workflows/03-reply-tracker.json:4
Finding
Unauthenticated Reply Webhook Allows Unauthorized CRM Modification and Notification Flooding<![CDATA[ ## Vulnerability Details **File Location**: `workflows/03-reply-tracker.json`, lines 4–15, 22–30, 69–127, and 137–139 **Vulnerability Type**: Missing webhook authentication and authorization **Risk Level**: High ### Vulnerable Code ```json { "parameters": { "httpMethod": "POST", "path": "outreach/reply", "responseMode": "responseNode", "options": {} }, "id": "n1", "name": "Reply Webhook", "type": "n8n-nodes-base.webhook", "typeVersion": 2 } ``` ```js const body = $input.first().json.body || $input.first().json; const email = (body.email || body.from || '').trim().toLowerCase(); const subject = (body.subject || '').trim(); const message = (body.message || body.body || '').trim(); if (!email) { return [{ json: { error: 'Email is required', valid: false } }]; } ``` ```json { "parameters": { "operation": "appendOrUpdate", "documentId": { "__rl": true, "value": "YOUR_OUTREACH_SHEET_ID", "mode": "list" }, "sheetName": { "__rl": true, "value": "Prospects", "mode": "list" }, "columns": { "mappingMode": "defineBelow", "value": { "email": "={{ $json.email }}", "replied": true, "replied_at": "={{ $json.replied_at }}", "status": "replied" }, "matchingColumns": [ "email" ] } }, "name": "Mark Replied", "type": "n8n-nodes-base.googleSheets" } ``` ```json { "parameters": { "sendTo": "={{ $env.OUTREACH_ADMIN_EMAIL || 'YOUR_NOTIFICATION_EMAIL' }}", "subject": "=Outreach Reply: {{ $json.email }}", "message": "=<h3>Reply Received</h3><p><strong>From:</strong> {{ $json.email }}</p><p><strong>Subject:</strong> {{ $json.subject }}</p><p><strong>Message:</strong></p><blockquote>{{ $json.message }}</blockquote><p><strong>Time:</strong> {{ $json.replied_at }}</p>" }, "name": "Notify Team", "type": "n8n-nodes-base.emailSend" } ``` ### Technical Analysis The `outreach/reply` webhook ...[truncated 2022 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Require authentication at the webhook boundary. Prefer a cryptographically verified signature from the inbound-mail provider; otherwise, require a high-entropy shared secret in an HTTP header. 2. Validate signatures over the raw request body and use a constant-time comparison. 3. Add a timestamp and unique event identifier to reject expired or replayed requests. 4. Replace `appendOrUpdate` with an update operation that fails when the prospect does not already exist. 5. Confirm that the submitted sender matches an existing prospect before changing campaign state. 6. Apply endpoint and infrastructure rate limits, request-body size limits, and per-source throttling. 7. Return `401 Unauthorized` or `403 Forbidden` for authentication failures rather than processing the event. 8. Restrict the Google Sheets credential to only the required document and minimum necessary permissions. 9. Monitor excessive reply events, failed signature checks, and abnormal SMTP volume. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
workflows/03-reply-tracker.json:137
Finding
Attacker-Controlled Reply Content Is Embedded in Administrator HTML Email Without Escaping<![CDATA[ ## Vulnerability Details **File Location**: `workflows/03-reply-tracker.json`, lines 23 and 137–139 **Vulnerability Type**: HTML injection into a trusted notification channel **Risk Level**: Medium ### Vulnerable Code ```js const body = $input.first().json.body || $input.first().json; const email = (body.email || body.from || '').trim().toLowerCase(); const subject = (body.subject || '').trim(); const message = (body.message || body.body || '').trim(); if (!email) { return [{ json: { error: 'Email is required', valid: false } }]; } return [{ json: { email, subject, message, replied_at: new Date().toISOString(), valid: true } }]; ``` ```json { "parameters": { "sendTo": "={{ $env.OUTREACH_ADMIN_EMAIL || 'YOUR_NOTIFICATION_EMAIL' }}", "subject": "=Outreach Reply: {{ $json.email }}", "message": "=<h3>Reply Received</h3><p><strong>From:</strong> {{ $json.email }}</p><p><strong>Subject:</strong> {{ $json.subject }}</p><p><strong>Message:</strong></p><blockquote>{{ $json.message }}</blockquote><p><strong>Time:</strong> {{ $json.replied_at }}</p>", "options": {} }, "id": "n5", "name": "Notify Team", "type": "n8n-nodes-base.emailSend", "typeVersion": 2.1 } ``` ### Technical Analysis The webhook reads `email`, `subject`, and `message` directly from an untrusted request. These values are interpolated into an HTML-formatted administrator email without HTML encoding or sanitization. An attacker can inject arbitrary HTML elements, including deceptive links, externally hosted images, and tracking pixels. Modern email clients commonly block scripts, so arbitrary JavaScript execution is not established by the audited code. However, email-client sanitization does not reliably prevent phishing content, misleading formatting, unsafe links, or privacy leakage through remote resources. This issue is amplified by the absence of authentication on the reply webhook, although HTML encoding remains necessary even aft ...[truncated 1385 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. HTML-encode every untrusted value before inserting it into an HTML template, including `email`, `subject`, `message`, and any provider-supplied metadata. 2. Prefer a plain-text notification body for raw inbound reply content. 3. If HTML must be retained, use a restrictive allowlist sanitizer that removes links, images, styles, forms, embedded resources, and active content. 4. Enforce reasonable maximum lengths for all webhook fields to reduce abuse and oversized notifications. 5. Authenticate and rate-limit the reply webhook as described in the related access-control finding. 6. Consider attaching the original message as plain text instead of rendering it inline. 7. Add a visible warning that notification content originates from an external, untrusted sender. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
workflows/01-prospect-import.json:23
Finding
Predictable Fallback Secret Causes Prospect Import Authentication to Fail Open on Misconfiguration<![CDATA[ ## Vulnerability Details **File Location**: `workflows/01-prospect-import.json`, line 23 **Vulnerability Type**: Predictable fallback authentication secret **Risk Level**: Medium ### Vulnerable Code ```js const body = $input.first().json.body || $input.first().json; const prospects = Array.isArray(body.prospects) ? body.prospects : [body]; const secret = (body._secret || '').trim(); if (!secret || secret !== ($env.OUTREACH_SECRET || 'YOUR_OUTREACH_SECRET')) { return [{ json: { error: 'Unauthorized', valid: false, prospects: [] } }]; } const validated = []; const errors = []; for (const p of prospects) { const email = (p.email || '').trim().toLowerCase(); if (!email || !email.includes('@')) { errors.push(`Invalid email: ${p.email || 'empty'}`); continue; } validated.push({ email, name: (p.name || '').trim(), company: (p.company || '').trim(), title: (p.title || '').trim(), campaign: (p.campaign || 'default').trim(), status: 'new', step: 0, last_sent_at: '', next_send_at: new Date().toISOString(), replied: false, bounced: false, imported_at: new Date().toISOString() }); } ``` ### Technical Analysis Authentication compares the supplied `_secret` against: ```js $env.OUTREACH_SECRET || 'YOUR_OUTREACH_SECRET' ``` When `OUTREACH_SECRET` is absent or empty, the accepted secret becomes the literal, publicly visible value `YOUR_OUTREACH_SECRET`. This converts an ordinary deployment mistake into predictable authentication. After authentication, accepted prospect data is saved using an `appendOrUpdate` operation keyed by email. Newly imported records have `next_send_at` set to the current time. The scheduled outreach workflow can therefore send email to attacker-selected recipients after the next execution. The weakness is conditional: exploitation requires the operator to activate the workflow without setting a nonempty `OUTREACH_SECRET`. The documentation declares the variable as requ ...[truncated 1553 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Fail closed when `OUTREACH_SECRET` is unset: ```js const configuredSecret = $env.OUTREACH_SECRET; if (!configuredSecret || configuredSecret === 'YOUR_OUTREACH_SECRET') { throw new Error('OUTREACH_SECRET is not securely configured'); } ``` 2. Reject known placeholders and secrets below an appropriate entropy or length threshold. 3. Pass the secret in an authorization header rather than in the JSON body to reduce accidental logging and exposure. 4. Compare secrets using a constant-time comparison where the n8n runtime permits it. 5. Add webhook-level authentication, infrastructure rate limiting, request-size limits, and import batch-size limits. 6. Prevent activation or deployment through a startup/configuration validation check when required environment variables are missing. 7. Restrict which campaigns or recipient domains can be imported where operationally appropriate. 8. Monitor unusual import volume and outbound-mail spikes. 9. Rotate the secret immediately if the placeholder was ever used in a deployed workflow. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
Findings (9)

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The skill automates collection, storage, and processing of personal contact data for unsolicited outreach, but it does not warn users about privacy, consent, anti-spam, or jurisdiction-specific compliance requirements. In context, this is dangerous because the workflow is explicitly designed for bulk cold-email sequencing and reply tracking, which can lead to unlawful processing of personal data, policy violations, or misuse at scale if deployed without safeguards.

External Transmission

Medium
Category
Data Exfiltration
Content
### 4. Import Prospects
```bash
curl -X POST https://your-n8n.com/webhook/outreach/import \
  -H "Content-Type: application/json" \
  -d '{
    "_secret": "your-outreach-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.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
This JSON workflow performs a data write operation by appending or updating prospect records in Google Sheets, including emails and other contact details. In this file there is no confirmation step, user-facing log/notice, or descriptive comment warning that submitted webhook data will be stored externally.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
This JSON workflow sends emails via SMTP using prospect data from a Google Sheet, but the file contains no natural-language warning, confirmation step, or user-facing disclosure about transmitting recipient data or performing automated outreach. Because this is a manifest/config file with embedded operational definitions rather than a markdown description, there is no visible in-file notice to alert operators about the behavior.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
The webhook accepts reply data and forwards the sender email, subject, and full message body to an admin email address without any authentication, consent notice, or minimization. If the webhook is exposed or abused, attacker-supplied content and potentially sensitive correspondence can be relayed externally through SMTP, creating a privacy leak and data-handling risk.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The workflow automatically emails aggregated outreach metrics derived from the Prospects sheet, which may contain sensitive business or prospect-related data, to a recipient without any validation, minimization, or approval step. If the SMTP recipient is misconfigured or the mailbox is insecure, internal campaign activity and prospect information can be exposed to unauthorized parties on a recurring basis.

Context-Inappropriate Capability

Low
Confidence
79% confidence
Finding
The manifest describes reply tracking, bounce handling, campaign analytics, Google Sheets CRM, and SMTP integration for outreach automation. In this workflow, SMTP is used to forward full reply contents to an admin notification address, which is an additional alerting/forwarding capability beyond simply tracking replies in the CRM.

Vague Triggers

Low
Confidence
79% confidence
Finding
This JSON workflow runs from a generic daily schedule trigger, but the manifest-style description in the file does not define any scope constraints, exclusions, or operator expectations around when the workflow should or should not run. For manifest/config files, vague activation conditions can lead to unintended execution because there is no documented boundary beyond a 24-hour interval.

Missing User Warnings

Low
Confidence
86% confidence
Finding
The recipient address is taken from an environment variable, which creates a covert configuration path for where reports are sent and can redirect data externally if the environment is altered or misconfigured. In this workflow, that risk is amplified because the value controls automatic delivery of prospect-derived reporting data without any in-workflow verification or disclosure.

Static analysis

No suspicious patterns detected.