Back to skill

Security audit

Google Sheets Reporting

Security checks for vulnerabilities and agentic risk

Overview

This skill does what it says, but it automatically emails Google Sheets-derived data and has unescaped HTML in those emails, so users should review it carefully before use.

Use this only for Google Sheets approved for automated email reporting. Set REPORT_EMAIL deliberately, restrict SMTP and Google Sheets credentials to the minimum needed, avoid sensitive personal or financial fields unless approved, and HTML-escape or convert reports to plain text before production use.

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

Warning
Location
workflows/01-daily-summary.json:56
Finding
HTML Email Injection Through Daily Summary Spreadsheet Data<![CDATA[ ## Vulnerability Details **File Location**: `workflows/01-daily-summary.json:56-72` **Vulnerability Type**: Unescaped HTML content injection **Risk Level**: Medium ### Vulnerable Code ```javascript // Build summary HTML table let summaryHtml = '<table border="1" cellpadding="8" cellspacing="0"><tr><th>Column</th><th>Count</th><th>Sum</th><th>Average</th><th>Min</th><th>Max</th></tr>'; for (const [col, s] of Object.entries(stats)) { summaryHtml += `<tr><td><strong>${col}</strong></td><td>${s.count}</td><td>${s.sum}</td><td>${s.avg}</td><td>${s.min}</td><td>${s.max}</td></tr>`; } summaryHtml += '</table>'; // Count unique values for first text column let categorySummary = ''; if (textCols.length > 0) { const firstTextCol = textCols[0]; const valueCounts = {}; for (const r of rows) { const v = (r[firstTextCol] || 'empty').toString(); valueCounts[v] = (valueCounts[v] || 0) + 1; } categorySummary = Object.entries(valueCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 10) .map(([v, c]) => `${v}: ${c}`) .join(', '); } ``` The generated values are subsequently inserted into the HTML email: ```json "message": "=<h2>Daily Data Summary</h2><p><strong>Date:</strong> {{ $json.report_date }}</p><p><strong>Total Rows:</strong> {{ $json.total_rows }}</p><p><strong>Numeric Columns:</strong> {{ $json.numeric_columns }}</p><h3>Column Statistics</h3>{{ $json.summary_html }}<h3>Top Categories ({{ $json.category_column }})</h3><p>{{ $json.category_summary }}</p>" ``` ### Technical Analysis The workflow constructs an HTML email by directly interpolating Google Sheets column names and cell values. The variables `col`, `v`, `category_column`, and `category_summary` can contain spreadsheet-controlled content, but no HTML encoding or sanitization is applied before the resulting strings are passed to the email node. An attacker with write access to the monitored sheet can place HTML markup in a column header or category value. That marku ...[truncated 1239 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Apply contextual HTML encoding to every spreadsheet-derived value before interpolation, including column names, category names, and category values. 2. Encode at least `&`, `<`, `>`, `"`, and `'`. 3. Prefer an email template system that escapes values by default rather than manually constructing HTML strings. 4. If rich formatting is unnecessary, send the report as plain text. 5. Do not sanitize the finished HTML with simple regular expressions; encode individual untrusted values at their insertion points. 6. Add tests using spreadsheet values containing HTML tags, links, quotes, ampersands, and image elements. 7. Consider disabling remote content in organizational email clients as defense in depth. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
workflows/02-threshold-alerts.json:56
Finding
HTML Email Injection Through Threshold Alert Identifiers<![CDATA[ ## Vulnerability Details **File Location**: `workflows/02-threshold-alerts.json:56-117` **Vulnerability Type**: Unescaped HTML content injection **Risk Level**: Medium ### Vulnerable Code ```javascript if (config.min !== null && config.min !== undefined && value < config.min) { alerts.push({ column: label, value, threshold: config.min, direction: 'below minimum', row_id: row.id || row.name || row.email || JSON.stringify(row).slice(0, 50) }); } if (config.max !== null && config.max !== undefined && value > config.max) { alerts.push({ column: label, value, threshold: config.max, direction: 'above maximum', row_id: row.id || row.name || row.email || JSON.stringify(row).slice(0, 50) }); } let alertHtml = '<table border="1" cellpadding="8" cellspacing="0"><tr><th>Column</th><th>Value</th><th>Threshold</th><th>Direction</th><th>Row</th></tr>'; for (const a of alerts) { alertHtml += `<tr><td>${a.column}</td><td><strong>${a.value}</strong></td><td>${a.threshold}</td><td>${a.direction}</td><td>${a.row_id}</td></tr>`; } alertHtml += '</table>'; ``` The resulting markup is sent directly as an HTML email: ```json "message": "=<h2>Threshold Alert</h2><p><strong>Checked at:</strong> {{ $json.checked_at }}</p><p><strong>Violations found:</strong> {{ $json.alert_count }}</p>{{ $json.alert_html }}<p><em>Configure thresholds via the ALERT_THRESHOLDS environment variable.</em></p>" ``` ### Technical Analysis The `row_id` value is selected from spreadsheet-controlled `id`, `name`, or `email` fields. If those fields are absent, a serialized portion of the entire row is used. The selected value is inserted directly into an HTML table without escaping. The `column` value can also originate from the `label` property of the `ALERT_THRESHOLDS` configuration and is not encoded. The spreadsheet-controlled row identifier is the principal exploitable input for an attacker with sheet write access. An attacker can combine ...[truncated 1268 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. HTML-encode `a.row_id`, `a.column`, `a.value`, `a.threshold`, and all other dynamic values before adding them to `alertHtml`. 2. Define and consistently use a helper such as: ```javascript function escapeHtml(value) { return String(value) .replaceAll('&', '&amp;') .replaceAll('<', '&lt;') .replaceAll('>', '&gt;') .replaceAll('"', '&quot;') .replaceAll("'", '&#39;'); } ``` 3. Validate `ALERT_THRESHOLDS` against a strict schema. Require expected property types and constrain labels to a reasonable length. 4. Use an immutable internal row identifier where possible instead of including arbitrary row data or `JSON.stringify(row)` in notifications. 5. Limit the maximum length of displayed identifiers to prevent oversized or misleading email content. 6. Prefer an automatically escaping template engine or plain-text alerts. 7. Add security tests in which threshold-violating rows contain HTML in `id`, `name`, and `email`. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
workflows/03-weekly-digest.json:86
Finding
HTML Email Injection Through Weekly Digest Column Headers<![CDATA[ ## Vulnerability Details **File Location**: `workflows/03-weekly-digest.json:86-101` **Vulnerability Type**: Unescaped HTML content injection **Risk Level**: Medium ### Vulnerable Code ```javascript function summarize(rows, label) { if (rows.length === 0) return `<p><strong>${label}:</strong> No data</p>`; const columns = Object.keys(rows[0]); const numericCols = columns.filter(col => { const sample = rows.slice(0, 10).map(r => r[col]).filter(v => v !== '' && v != null); return sample.length > 0 && sample.every(v => !isNaN(parseFloat(v))); }); let html = `<h3>${label} (${rows.length} rows)</h3>`; if (numericCols.length === 0) { return html + '<p>No numeric columns found</p>'; } html += '<table border="1" cellpadding="8" cellspacing="0"><tr><th>Metric</th><th>Sum</th><th>Average</th><th>Min</th><th>Max</th></tr>'; for (const col of numericCols) { const vals = rows.map(r => parseFloat(r[col])).filter(v => !isNaN(v)); if (vals.length === 0) continue; const sum = Math.round(vals.reduce((a, b) => a + b, 0) * 100) / 100; const avg = Math.round(sum / vals.length * 100) / 100; html += `<tr><td><strong>${col}</strong></td><td>${sum}</td><td>${avg}</td><td>${Math.min(...vals)}</td><td>${Math.max(...vals)}</td></tr>`; } html += '</table>'; return html; } ``` The generated summaries are inserted directly into the outgoing HTML email: ```json "message": "=<h2>Weekly Data Digest</h2><p><strong>Period:</strong> {{ $json.week_start }} to {{ $json.report_date }}</p>{{ $json.primary_summary }}{{ $json.secondary_summary }}<hr><p><em>This report is generated automatically every Monday at 9 AM.</em></p>" ``` ### Technical Analysis Google Sheets column headers become JavaScript object keys and are recovered through `Object.keys(rows[0])`. A header classified as numeric is later interpolated as `${col}` inside the HTML summary table. The code does not encode the header before inserting it into the email. An ...[truncated 1273 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. HTML-encode every column name before adding it to the summary table. 2. Encode all future spreadsheet-derived textual values, even if the current implementation only displays headers and numeric results. 3. Use a template mechanism that escapes variables by default. 4. Restrict accepted column names to an explicit allowlist when the expected schema is known. 5. If automatic schema detection is required, reject or safely encode control characters and HTML metacharacters in displayed labels. 6. Add tests using malicious primary and secondary sheet headers while retaining numeric column values. 7. Consider plain-text digest messages where HTML formatting is not essential. ]]>
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 (7)

Missing User Warnings

Medium
Confidence
91% confidence
Finding
The skill is designed to read arbitrary Google Sheets data and email summaries/alerts, but the documentation provides no warning that sheet contents may include sensitive business or personal data that will be transmitted over email. In this context, the omission is meaningful because users are encouraged to apply the workflows to 'any Google Sheet' and multiple operational use cases, increasing the chance of unintentional disclosure through broadly delivered reports or insecure email handling.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
This workflow runs on a schedule, reads all data from a Google Sheet, and sends spreadsheet-derived content to an external email recipient, potentially controlled by an environment variable. Because the email body includes sheet-derived fields rendered as HTML/text without any approval step, allowlisting, minimization, or disclosure, sensitive business or personal data could be exfiltrated automatically if the sheet contains unexpected data or the recipient is misconfigured.

Vague Triggers

Medium
Confidence
90% confidence
Finding
This manifest-style JSON names the skill as a generic threshold alert workflow but provides no explicit invocation scope, exclusions, or constraints beyond the title. For manifest files, the absence of specific trigger phrases or limiting context can lead to ambiguous activation or reuse in unintended situations.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The workflow emails sheet-derived content, including row identifiers chosen from fields like id, name, or email, directly into an HTML message. Because this data may contain personal or sensitive business information and is sent automatically without minimization or disclosure, it can expose internal data to unintended recipients and create privacy/compliance risk.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
This workflow reads data from two Google Sheets, summarizes it, and sends the resulting content by email on a schedule. The file contains no confirmation step, visible notification, or explanatory comment/docstring warning that spreadsheet data will be transmitted externally via SMTP.

Context-Inappropriate Capability

Medium
Confidence
84% confidence
Finding
The manifest describes automated reporting from Google Sheets with digest emails, but this workflow also accesses process environment data via $env.REPORT_EMAIL. Reading environment variables is not an obvious requirement of analyzing Sheets data or composing a digest, and it expands the skill's access beyond the stated business function.

Missing User Warnings

Low
Confidence
77% confidence
Finding
The workflow reads the REPORT_EMAIL environment variable to determine where to send the digest. Although this is not a secret itself in all cases, the file provides no warning or comment that runtime environment configuration influences outbound delivery of report data.

Static analysis

No suspicious patterns detected.