Back to skill

Security audit

Cold Email Outreach

Security checks for vulnerabilities and agentic risk

Overview

This cold-email skill largely does what it advertises, but it should be reviewed because it uploads prospect contact data to external services and handles API keys with limited safeguards.

Review this before installing. Use it only if you are authorized to process and upload the lead data, have checked applicable privacy and anti-spam obligations, and are comfortable sharing the data with Hunter and Instantly. Store API keys outside the repo or add a strong ignore rule, test with a small CSV first, and consider changing the script to fail closed on Hunter errors and require an explicit upload confirmation.

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

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/import-csv.js:60
Finding
Email Verification Fails Open on Hunter API Errors<![CDATA[ ## Vulnerability Details **File Location**: `scripts/import-csv.js:60-69` **Vulnerability Type**: Fail-open validation caused by improper HTTP error handling **Risk Level**: Medium ### Complete Code Snippet ```js async function verifyEmail(email) { if (!email || !email.includes("@")) return false; const res = await request({ hostname: "api.hunter.io", path: `/v2/email-verifier?email=${encodeURIComponent(email)}&api_key=${config.hunter.apiKey}`, method: "GET", }); if (res.status !== 200) return true; const status = res.body?.data?.status; return ["valid", "accept_all"].includes(status); } ``` ### Technical Analysis The verification function treats every non-200 HTTP response from Hunter as successful verification: ```js if (res.status !== 200) return true; ``` Consequently, authentication failures, authorization errors, rate limiting, malformed requests, upstream server failures, and service outages all cause the submitted email address to be classified as verified. Network-level errors emitted by `https.request` are rejected separately, but completed HTTP responses with error status codes fail open. This violates the expected security property of an email-verification control: when verification cannot be performed, the result should be unknown or rejected rather than accepted. The resulting lead is added to `verifiedLeads` and later transmitted to Instantly. ### Attack Path 1. An attacker or operational condition causes Hunter to return a non-200 response. Examples include exhausting the account's quota, using an invalid API key, triggering rate limiting, or encountering an upstream service failure. 2. `verifyEmail()` receives the error response. 3. The condition `res.status !== 200` evaluates to true. 4. The function returns `true`, incorrectly marking the email as verified. 5. The calling loop adds the lead and its associated name, company, and personalization data to `verifiedLeads`. 6. `uploadLead()` sends the un ...[truncated 898 chars]
Remediation
<![CDATA[ ## Remediation Suggestions Fail closed whenever verification cannot be completed: ```js async function verifyEmail(email) { if (!email || !email.includes("@")) { return { verified: false, reason: "invalid_format" }; } const res = await request({ hostname: "api.hunter.io", path: `/v2/email-verifier?email=${encodeURIComponent(email)}&api_key=${config.hunter.apiKey}`, method: "GET", }); if (res.status !== 200) { throw new Error(`Hunter verification failed with HTTP ${res.status}`); } const status = res.body?.data?.status; return { verified: ["valid", "accept_all"].includes(status), reason: status || "unknown", }; } ``` Additional hardening should include: 1. Distinguish definitive invalid results from temporary verification failures. 2. Implement bounded retries with exponential backoff for HTTP 429 and transient 5xx responses. 3. Stop or pause the import if verification is unavailable rather than silently accepting leads. 4. Report authentication and quota failures clearly to the operator. 5. Record leads with unknown verification state separately and prevent their upload by default. 6. Add automated tests covering 200, 401, 403, 429, and 5xx responses. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/config.example.js:1
Finding
API Credentials Are Stored in a Source-Tree Configuration File and Exposed in a Query String<![CDATA[ ## Vulnerability Details **File Locations**: - `SKILL.md:20-24` - `scripts/config.example.js:1-10` - `scripts/import-csv.js:60-66` **Vulnerability Type**: Insecure secret storage and credential placement **Risk Level**: Medium ### Complete Code Snippets The installation instructions direct users to create a credential-bearing file inside the source tree: ```md ## Setup ```bash # 1. Edit config.js — add your keys cp scripts/config.example.js scripts/config.js # Fill in: instantly.apiKey, hunter.apiKey, apollo.apiKey, target ICP ``` ``` The configuration template stores credentials as JavaScript string literals: ```js // Copy this to config.js and fill in your keys module.exports = { instantly: { apiKey: "YOUR_INSTANTLY_V2_BEARER_TOKEN", }, hunter: { apiKey: "YOUR_HUNTER_API_KEY", }, apollo: { apiKey: "YOUR_APOLLO_API_KEY", // Only needed for pipeline.js (API scrape mode) ``` The Hunter credential is included in the request URL: ```js async function verifyEmail(email) { if (!email || !email.includes("@")) return false; const res = await request({ hostname: "api.hunter.io", path: `/v2/email-verifier?email=${encodeURIComponent(email)}&api_key=${config.hunter.apiKey}`, method: "GET", }); ``` ### Technical Analysis Users are instructed to copy the example configuration to `scripts/config.js` and place live API credentials directly into it. No `.gitignore` file was present in the audited project to exclude `scripts/config.js`. A user following the documented workflow may therefore accidentally commit, archive, upload, or otherwise distribute live credentials with the project. The Hunter key is also embedded in the URL query string. HTTPS protects the URL during transport, so passive network observers cannot ordinarily read it. However, complete request URLs can be retained by application diagnostics, reverse proxies, API gateways, monitoring systems, exception reports, or server access logs. Query-string cr ...[truncated 1970 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Load credentials from environment variables or a dedicated secret manager rather than a JavaScript file in the source tree: ```js module.exports = { instantly: { apiKey: process.env.INSTANTLY_API_KEY, }, hunter: { apiKey: process.env.HUNTER_API_KEY, }, apollo: { apiKey: process.env.APOLLO_API_KEY, }, }; ``` 2. Validate required variables at startup and terminate without printing their values: ```js for (const name of ["INSTANTLY_API_KEY", "HUNTER_API_KEY"]) { if (!process.env[name]) { throw new Error(`Required environment variable is missing: ${name}`); } } ``` 3. Add defensive ignore rules: ```gitignore .env .env.* !.env.example scripts/config.js ``` 4. Provide an `.env.example` that contains placeholder variable names only. 5. Use an authorization header for Hunter if supported by the API. If query-string authentication is mandatory, ensure that request URLs are redacted in application logs, proxy logs, monitoring tools, and error reports. 6. Never print request options, authorization headers, configuration objects, or complete authenticated URLs. 7. Use narrowly scoped credentials where supported, rotate them periodically, and revoke them immediately after suspected disclosure. 8. Add secret scanning to version-control hooks and continuous integration to detect committed tokens. 9. Update the setup documentation to explain secure secret provisioning and warn users not to commit local credential files. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Supply ChainUnpinned Dependencies, External Script Fetching, Obfuscated Code
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
Findings (3)

Missing User Warnings

Medium
Confidence
92% confidence
Finding
The skill explicitly automates scraping leads, verifying their email addresses, and uploading that contact data to third-party platforms, but it provides no warning about privacy, consent, terms-of-service, or data-sharing implications. This can cause users to transfer personal/business contact information to external services without understanding compliance, reputational, or account-risk consequences.

Missing User Warnings

Medium
Confidence
94% confidence
Finding
The script sends lead email addresses to Hunter's external verification API automatically, with no user-facing notice, consent check, or configuration gate indicating that third-party transfer will occur. Because the CSV likely contains personal data from prospects, this creates privacy, compliance, and confidentiality risk if the operator is unaware of the external processing or lacks a lawful basis to share the data.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The script uploads verified leads' personal data, including email, names, company, and generated personalization text, to Instantly without an explicit warning that this information is being transferred to a third-party campaign platform. In a lead-import workflow this behavior is functionally intended, but the lack of transparent disclosure and confirmation can still cause unauthorized sharing of personal data and regulatory or contractual violations.

Static analysis

No suspicious patterns detected.