Back to skill

Security audit

Appointment Booking System

Security checks for vulnerabilities and agentic risk

Overview

The skill matches an appointment-booking purpose, but its public n8n webhooks can send email and change booking records with too little authentication, validation, and privacy guidance.

Install only after adding authentication or signed expiring tokens for status changes, rate limiting and anti-abuse controls for public intake, strict field validation and length limits, HTML and spreadsheet-formula escaping, and a clear privacy/retention policy for customer data. Use least-privilege Google Sheets and SMTP credentials and test with non-production data first.

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

T05 · Unauthorized Access and Privilege Escalation

Error
Location
workflows/02-booking-confirmation.json:5
Finding
Unauthenticated Booking Status Modification<![CDATA[ ## Vulnerability Details **File Location**: `workflows/02-booking-confirmation.json:5-11, 21-35, 79-119` **Vulnerability Type**: Missing authentication and authorization on a state-changing webhook **Risk Level**: High ### Vulnerable Code ```json { "parameters": { "httpMethod": "POST", "path": "booking/confirm", "responseMode": "responseNode", "options": {} }, "id": "n1", "name": "Confirm Webhook", "type": "n8n-nodes-base.webhook", "typeVersion": 2 } ``` ```javascript const body = $input.first().json.body || $input.first().json; const bookingId = (body.booking_id || '').trim(); const action = (body.action || 'confirm').trim(); if (!bookingId) { return [{ json: { valid: false, error: 'booking_id is required' } }]; } return [{ json: { valid: true, booking_id: bookingId, action, new_status: action === 'cancel' ? 'cancelled' : 'confirmed', updated_at: new Date().toISOString() } }]; ``` ```json { "parameters": { "operation": "appendOrUpdate", "documentId": { "__rl": true, "value": "YOUR_BOOKING_SHEET_ID", "mode": "list" }, "sheetName": { "__rl": true, "value": "Appointments", "mode": "list" }, "columns": { "mappingMode": "defineBelow", "value": { "booking_id": "={{ $json.booking_id }}", "status": "={{ $json.new_status }}", "updated_at": "={{ $json.updated_at }}" }, "matchingColumns": [ "booking_id" ] } }, "name": "Update Status", "type": "n8n-nodes-base.googleSheets" } ``` ### Technical Analysis The `booking/confirm` webhook performs a privileged state-changing operation without authenticating the caller or checking whether the caller owns or is authorized to manage the specified booking. Possession of a booking ID is treated as sufficient authority. The request parser accepts any action string and maps only the exact value `cancel` to `cancelled`; every other val ...[truncated 1673 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Require authentication on the webhook, such as an authenticated staff session or a verified service-to-service credential. 2. For client-accessible confirmation or cancellation links, issue a unique, high-entropy, booking-specific token and store only a cryptographic hash of it. 3. Alternatively, sign the booking ID, permitted action, and expiration time with an HMAC secret and verify the signature before performing an update. 4. Authorize the requested action against the relevant booking rather than treating knowledge of its ID as authorization. 5. Restrict `action` to an explicit allowlist such as `confirm` and `cancel`; reject all other values. 6. Read and verify that the booking exists before modifying it. 7. Replace `appendOrUpdate` with update-only behavior so unknown identifiers cannot create partial rows. 8. Add expiration, replay protection, rate limiting, audit logging, and alerts for repeated invalid requests. 9. Return a generic error response that does not reveal whether a guessed booking ID exists. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
workflows/01-booking-intake.json:21
Finding
Stored HTML Injection in Client and Staff Emails<![CDATA[ ## Vulnerability Details **File Location**: `workflows/01-booking-intake.json:21-49, 187-227`; `workflows/03-reminder-engine.json:133-139`; `workflows/04-noshow-followup.json:140-146`; `workflows/05-daily-schedule.json:53-59, 89-95` **Vulnerability Type**: Improper neutralization of attacker-controlled content in HTML email **Risk Level**: Medium ### Vulnerable Code The intake workflow accepts fields as unrestricted strings: ```javascript const body = $input.first().json.body || $input.first().json; const name = (body.name || '').trim(); const email = (body.email || '').trim().toLowerCase(); const phone = (body.phone || '').trim(); const service = (body.service || '').trim(); const date = (body.date || '').trim(); const time = (body.time || '').trim(); const notes = (body.notes || '').trim(); const errors = []; if (!name) errors.push('Name is required'); if (!email || !email.includes('@')) errors.push('Valid email is required'); if (!phone) errors.push('Phone is required'); if (!service) errors.push('Service type is required'); if (!date) errors.push('Date is required'); if (!time) errors.push('Time is required'); ``` Those values are interpolated directly into HTML email: ```json { "sendTo": "={{ $env.STAFF_EMAIL || 'YOUR_NOTIFICATION_EMAIL' }}", "subject": "=New Booking: {{ $json.name }} — {{ $json.service }} on {{ $json.date }} at {{ $json.time }}", "message": "=<h2>New Appointment Booked</h2><table border='1' cellpadding='8' cellspacing='0'><tr><td><strong>Booking ID</strong></td><td>{{ $json.booking_id }}</td></tr><tr><td><strong>Client</strong></td><td>{{ $json.name }}</td></tr><tr><td><strong>Email</strong></td><td>{{ $json.email }}</td></tr><tr><td><strong>Phone</strong></td><td>{{ $json.phone }}</td></tr><tr><td><strong>Service</strong></td><td>{{ $json.service }}</td></tr><tr><td><strong>Date</strong></td><td>{{ $json.date }}</td></tr><tr><td><strong>Time</strong></td><td>{{ $json.time }}</td></tr><tr><td><strong>Notes</strong></ ...[truncated 2968 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. HTML-encode every untrusted field at the final output boundary. At minimum, encode `&`, `<`, `>`, `"`, and `'`. 2. Use an email templating mechanism with automatic contextual escaping rather than directly building HTML with string interpolation. 3. Apply encoding in every workflow that produces HTML, including intake confirmation, staff notification, reminders, no-show follow-up, and daily schedules. 4. Validate fields against their intended formats: - Restrict dates and times to strict formats. - Validate phone numbers and email addresses. - Restrict service values to a server-controlled allowlist where possible. 5. Enforce conservative length limits for all booking fields, especially `notes`. 6. If limited formatting is required, sanitize it with a strict allowlist that excludes remote images, forms, style attributes, and unsafe URI schemes. 7. Sanitize historical rows already present in the sheet or guarantee that all values are escaped when rendered. 8. Add automated tests containing HTML metacharacters and malicious link/image payloads. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
workflows/01-booking-intake.json:5
Finding
Public Booking Intake Can Be Abused as an Email and Resource-Consumption Relay<![CDATA[ ## Vulnerability Details **File Location**: `workflows/01-booking-intake.json:5-11, 21-49, 181-207, 309-331` **Vulnerability Type**: Missing abuse controls on a public email-producing endpoint **Risk Level**: Medium ### Vulnerable Code The intake endpoint is publicly callable and has no configured authentication or rate-control option: ```json { "parameters": { "httpMethod": "POST", "path": "booking/new", "responseMode": "responseNode", "options": {} }, "id": "n1", "name": "Booking Webhook", "type": "n8n-nodes-base.webhook", "typeVersion": 2, "webhookId": "booking-intake" } ``` Email validation checks only for an `@` character: ```javascript const email = (body.email || '').trim().toLowerCase(); const errors = []; if (!name) errors.push('Name is required'); if (!email || !email.includes('@')) errors.push('Valid email is required'); if (!phone) errors.push('Phone is required'); if (!service) errors.push('Service type is required'); if (!date) errors.push('Date is required'); if (!time) errors.push('Time is required'); if (errors.length > 0) { return [{ json: { valid: false, errors } }]; } ``` Every accepted request can trigger an email to the submitted address: ```json { "parameters": { "sendTo": "={{ $json.email }}", "subject": "=Booking Confirmed — {{ $json.service }} on {{ $json.date }}", "message": "=<h2>Booking Confirmation</h2><p>Hi {{ $json.name }},</p><p>Your appointment has been confirmed:</p><table border='1' cellpadding='8' cellspacing='0'><tr><td><strong>Booking ID</strong></td><td>{{ $json.booking_id }}</td></tr><tr><td><strong>Service</strong></td><td>{{ $json.service }}</td></tr><tr><td><strong>Date</strong></td><td>{{ $json.date }}</td></tr><tr><td><strong>Time</strong></td><td>{{ $json.time }}</td></tr></table>", "options": {} }, "name": "Confirm to Client", "type": "n8n-nodes-base.emailSend" } ``` The saved booking also causes a staff notification: ```json "Save to Shee ...[truncated 2080 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Place the webhook behind gateway-level rate limiting based on source, recipient address, and business-defined quotas. 2. Add a CAPTCHA or equivalent proof-of-human challenge for public web forms. 3. Require email ownership verification before treating an appointment as confirmed or sending further reminders. 4. Apply strict request-body size and field-length limits before invoking storage or email nodes. 5. Use robust address parsing and validation, while recognizing that syntax validation alone does not prove ownership. 6. Add duplicate detection and cooldown periods for matching recipient, phone, date, and time combinations. 7. Consider initially storing bookings as `pending` and sending only a minimal verification message. 8. Limit SMTP account quotas and alert on unusual request, recipient, or bounce volumes. 9. Add queueing and backpressure so bursts cannot exhaust n8n workers. 10. Log abuse-relevant metadata while minimizing personal-data retention. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
workflows/01-booking-intake.json:21
Finding
Spreadsheet Formula Injection Through Booking Fields<![CDATA[ ## Vulnerability Details **File Location**: `workflows/01-booking-intake.json:21-49, 90-107` **Vulnerability Type**: Improper neutralization of formulas in spreadsheet-bound data **Risk Level**: Medium ### Vulnerable Code Attacker-controlled values are accepted without neutralizing spreadsheet formula prefixes: ```javascript const body = $input.first().json.body || $input.first().json; const name = (body.name || '').trim(); const email = (body.email || '').trim().toLowerCase(); const phone = (body.phone || '').trim(); const service = (body.service || '').trim(); const date = (body.date || '').trim(); const time = (body.time || '').trim(); const notes = (body.notes || '').trim(); const errors = []; if (!name) errors.push('Name is required'); if (!email || !email.includes('@')) errors.push('Valid email is required'); if (!phone) errors.push('Phone is required'); if (!service) errors.push('Service type is required'); if (!date) errors.push('Date is required'); if (!time) errors.push('Time is required'); ``` The values are then mapped directly into Google Sheets columns: ```json "value": { "booking_id": "={{ $json.booking_id }}", "name": "={{ $json.name }}", "email": "={{ $json.email }}", "phone": "={{ $json.phone }}", "service": "={{ $json.service }}", "date": "={{ $json.date }}", "time": "={{ $json.time }}", "notes": "={{ $json.notes }}", "status": "={{ $json.status }}", "reminder_24h": "={{ $json.reminder_24h }}", "reminder_2h": "={{ $json.reminder_2h }}", "created_at": "={{ $json.created_at }}" } ``` ### Technical Analysis Spreadsheet applications may interpret cells beginning with characters such as `=`, `+`, `-`, or `@` as formulas. The workflow accepts public input and writes it directly to Google Sheets without forcing literal-text semantics or prefix-neutralizing formula-like values. Actual formula evaluation depends on the Google Sheets node's write mode and Google Sheets API behavior. The workflow nevertheless l ...[truncated 1996 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Configure the Google Sheets write operation to use literal or raw text input semantics where supported. 2. Before storage, detect values whose first non-whitespace character is `=`, `+`, `-`, or `@`. 3. Prefix dangerous values with an apostrophe or otherwise escape them according to Google Sheets guidance. 4. Apply neutralization to every user-controlled field, including name, email, phone, service, date, time, and notes. 5. Enforce strict formats for phone, email, date, and time fields, and use a server-controlled allowlist for service names. 6. Preserve formula-neutralization during CSV or Excel export; exported files should also treat untrusted cells as text. 7. Review existing appointment rows for formula-prefixed content. 8. Add tests covering leading whitespace followed by formula characters and spreadsheet-specific formula syntax. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • 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
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
Findings (13)

Ae1

High
Category
analysis-evasion
Content
| 03 | `03-reminder-engine.json` | Hourly check → send 24h and 2h reminders |
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
| 04 | `04-noshow-followup.json` | Check past appointments → detect no-shows → followup email |
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Missing User Warnings

High
Confidence
96% confidence
Finding
This workflow collects, stores, and transmits sensitive personal data including name, email, phone, booking details, and freeform notes to Google Sheets and over email, but it contains no visible consent, minimization, retention, or sanitization controls. In this context, the skill is explicitly a production booking system, so handling customer PII without privacy safeguards or limiting untrusted content in emails/notes materially increases privacy, compliance, and data exposure risk.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The skill processes and stores personal data including names, emails, phone numbers, appointment dates, and notes, and transmits that data through webhooks, Google Sheets, and SMTP. The documentation does not warn users about privacy, consent, retention, or regulatory considerations, which increases the risk of unsafe deployment and inadvertent mishandling of PII.

External Transmission

Medium
Category
Data Exfiltration
Content
### 4. Test Booking
```bash
curl -X POST https://your-n8n.com/webhook/booking/new \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Jane Smith",
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Vague Triggers

Medium
Confidence
88% confidence
Finding
This JSON manifest defines activation via a public webhook path of "booking/new" but provides no additional scope, context, or exclusion conditions describing when this workflow should or should not be invoked. For a manifest/config file, that broad trigger surface can lead to unintended or unauthorized activation because the trigger is not narrowly constrained in the file itself.

Missing User Warnings

Medium
Confidence
91% confidence
Finding
This workflow performs a persistent data write by updating booking status records in Google Sheets after receiving a webhook request. In this JSON file there is no visible confirmation step, user-facing log/message, or inline documentation warning that incoming requests will modify stored booking data.

Missing User Warnings

Medium
Confidence
87% confidence
Finding
This code-path sends emails to customer addresses using names, appointment details, and business contact information, which is a network operation transmitting user data. The workflow contains no confirmation step, user-facing log/print, or explanatory comment/docstring warning that customer data will be sent externally via SMTP.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The workflow sends emails to customer addresses and includes personal and appointment details such as name, service, date, and time. In this JSON file there is no confirmation step, user-facing notice, or descriptive comment warning that customer data will be transmitted via SMTP.

Missing User Warnings

Medium
Confidence
86% confidence
Finding
This step writes back to Google Sheets and changes status fields to 'no-show' while recording a follow-up timestamp, which affects business records and may be difficult to undo cleanly. The file does not include a confirmation mechanism, user-facing log, or explanatory documentation warning that records will be modified automatically.

Missing User Warnings

Low
Confidence
81% confidence
Finding
This step writes back to Google Sheets by setting reminder status fields, which modifies user data. Although this is part of the workflow logic, the file provides no comment or user-facing disclosure that appointment records will be altered as reminders are processed.

Vague Triggers

Low
Confidence
88% confidence
Finding
This JSON manifest defines activation only as a cron expression running every day at 7:00, but provides no additional scope, negative examples, or contextual limits for when this workflow should or should not be used. Under the vague-trigger rule for manifest files, this is an ambiguous activation condition because the invocation intent is not further constrained in human-readable terms.

Natural-Language Policy Violations

Low
Confidence
95% confidence
Finding
The workflow hard-codes English phrases such as 'Today', 'Tomorrow', 'Daily Schedule Report', and 'No appointments scheduled' in generated content. Because there is no opt-in or locale selection mechanism, this is a language-policy issue under the natural-language policy rule.

Static analysis

No suspicious patterns detected.