Back to skill

Security audit

Lead Gen + CRM Pipeline

Security checks for vulnerabilities and agentic risk

Overview

The skill largely matches its lead-generation purpose, but it can send outreach emails, write CRM records, store credentials and lead data, and contact user-controlled destinations without enough enforced safeguards.

Review this skill before installing. Use it only in a controlled workspace, keep API tokens out of committed config files, restrict file permissions, validate CRM tenant and lead-domain values, run outreach only after explicit human review, and confirm templates include required opt-out and sender information. Avoid unattended campaign sending until approval and compliance checks are enforced in code.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/push-to-crm.sh:93
Finding
Credential and Lead Data Disclosure Through Pipedrive Endpoint Injection<![CDATA[ ## Vulnerability Details **File Location**: `scripts/push-to-crm.sh:93-107` **Vulnerability Type**: Unvalidated endpoint construction and sensitive-data disclosure **Risk Level**: High ### Vulnerable Code ```python api_key = crm_config.get("api_key") or os.environ.get("PIPEDRIVE_API_KEY", "") domain = crm_config.get("domain") or os.environ.get("PIPEDRIVE_DOMAIN", "") if not api_key or not domain: print(f" ⚠️ PIPEDRIVE_API_KEY/DOMAIN not set") return False try: resp = requests.post( f"https://{domain}.pipedrive.com/api/v1/persons", params={"api_token": api_key}, json={ "name": f"{first_name} {last_name}".strip() or company, "email": [{"value": email, "primary": True}], "org_id": None, }, timeout=10, ) ``` ### Technical Analysis The Pipedrive domain is read from `config.json` or `PIPEDRIVE_DOMAIN` and interpolated directly into a URL without validating that it is a simple Pipedrive tenant identifier. URL delimiters such as `/`, `?`, and `#` can cause the resulting URL to be interpreted with a host other than the intended `*.pipedrive.com` host. The request contains two sensitive data classes: - The Pipedrive API token is included in the query string. - Lead names and email addresses are included in the JSON body. Consequently, a malicious or corrupted domain value can cause both the credential and lead data to be sent to an unintended server. Sending lead information to the selected CRM is required by the Skill's functionality, but permitting the destination host to be changed through unrestricted string interpolation exceeds the minimum privilege required. ### Attack Path 1. An attacker who can influence `config.json`, `PIPEDRIVE_DOMAIN`, or the automation environment supplies a crafted domain containing URL delimiters. 2. A user or scheduled workflow invokes `push-to-crm.sh`. 3. The script concatenates the unvalidated value into the request URL. 4. ...[truncated 541 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Restrict the tenant value to a strict slug format, for example `^[A-Za-z0-9-]+$`. - Construct the URL with a URL-building library instead of string interpolation. - Parse the final URL and verify that its hostname is either `pipedrive.com` or ends exactly with `.pipedrive.com`. - Reject embedded credentials, path separators, query delimiters, fragments, and nonstandard ports. - Disable redirects or validate the destination hostname and resolved address before every redirect. - Avoid placing API credentials in query strings where possible; use the service's supported authorization header. - Store allowed CRM hosts in code rather than accepting an arbitrary host from configuration. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/enrich-leads.sh:85
Finding
Server-Side Request Forgery During Lead Website Enrichment<![CDATA[ ## Vulnerability Details **File Location**: `scripts/enrich-leads.sh:85-110` **Vulnerability Type**: Server-side request forgery through an unvalidated lead domain **Risk Level**: High ### Vulnerable Code ```python # Basic web scrape for additional signals if domain: try: resp = requests.get(f"https://{domain}", timeout=8, headers={"User-Agent": "ReighlanLeadBot/1.0"}) if resp.status_code == 200: text = resp.text.lower() # Tech signals tech = [] tech_checks = { "shopify": "shopify", "wordpress": "wp-content", "react": "react", "next.js": "_next", "hubspot": "hubspot", "intercom": "intercom", "google analytics": "google-analytics", "stripe": "stripe", } for name, sig in tech_checks.items(): if sig in text: tech.append(name) lead["tech_stack"] = tech # Social links socials = {} linkedin = re.search(r'linkedin\.com/company/([^/"]+)', text) if linkedin: socials["linkedin"] = f"https://linkedin.com/company/{linkedin.group(1)}" twitter = re.search(r'(?:twitter|x)\.com/([^/"]+)', text) if twitter: socials["twitter"] = f"https://x.com/{twitter.group(1)}" lead["social_profiles"] = socials except: pass ``` ### Technical Analysis The `domain` value is loaded from a lead JSON document and used directly as the target of an HTTPS request. The code does not validate: - Whether the value is a syntactically valid public hostname. - Whether it resolves to a loopback, private, link-local, reserved, or cloud metadata address. - Whether it contains credentials, a port, a path, or URL delimiters. - Whether an HTTP redirect leads to an internal address. - Whether DNS resolution changes between validation and connection. Lead records originate from external search results ...[truncated 1275 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Parse the target as a hostname rather than accepting a URL-like string. - Reject credentials, paths, query strings, fragments, IP literals, and unexpected ports. - Resolve the hostname before connecting and reject every loopback, private, link-local, multicast, reserved, unspecified, and cloud metadata address. - Validate all IPv4 and IPv6 results. - Disable automatic redirects or validate the hostname and resolved addresses at every redirect. - Protect against DNS rebinding by connecting only to a previously validated resolved address while preserving the intended TLS server name. - Consider using an outbound proxy with an allowlist limited to public HTTP and HTTPS destinations. - Log rejected destinations rather than suppressing all exceptions with a bare `except`. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/init-workspace.sh:8
Finding
Plaintext Service Credentials Created Without Restrictive File Permissions<![CDATA[ ## Vulnerability Details **File Location**: `scripts/init-workspace.sh:8-31` **Vulnerability Type**: Insecure local credential storage **Risk Level**: Medium ### Vulnerable Code ```bash if [ ! -f "$BASE_DIR/config.json" ]; then cat > "$BASE_DIR/config.json" << 'EOF' { "hunter_api_key": "", "default_crm": "hubspot", "crm": { "hubspot": { "api_key": "", "pipeline_id": "", "stage_id": "" }, "pipedrive": { "api_key": "", "domain": "" }, "zoho": { "access_token": "", "refresh_token": "" } }, "email": { "provider": "sendgrid", "sendgrid_api_key": "", "smtp": { "host": "", "port": 587, "user": "", "pass": "" }, "from_name": "", "from_email": "", "daily_limit": 50, "delay_between_emails_sec": 30 }, ``` ### Technical Analysis The generated `config.json` is explicitly designed to store Hunter, HubSpot, Pipedrive, Zoho, SendGrid, and SMTP credentials in plaintext. The initialization script does not establish a restrictive `umask` or apply `chmod 600` after creating the file. Under a common `umask` of `022`, shell redirection creates the file with mode `0644`, allowing other local users to read it. The containing workspace directories are also created without explicit restrictive permissions. ### Attack Path 1. A user initializes the workspace. 2. The script creates `config.json` according to the caller's current `umask`. 3. The user populates the placeholder fields with real API tokens or an SMTP password. 4. If the resulting mode permits group or world access, another local user or process reads the file. 5. The exposed credentials are reused against the corresponding external services. ### Impact Assessment Credential theft can grant access to email-sending infrastructure, CRM records, lead-enrichment services, and potentially long-lived Zoho sessions through a refresh token. The precise privileges depend on each credential's scopes, but compromise can result in unauthorized email delivery, CRM mo ...[truncated 57 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Prefer environment variables, operating-system credential stores, or a dedicated secrets manager. - Set `umask 077` before creating the workspace and configuration file. - Explicitly set directory permissions to `0700` and `config.json` permissions to `0600`. - Create the file atomically and refuse to follow symlinks. - Separate nonsecret configuration from credentials so ordinary settings can be handled without exposing secrets. - Document credential rotation procedures and recommend narrowly scoped service tokens. - Check existing file permissions at startup and refuse to use a configuration file that is readable by unauthorized principals. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/create-campaign.sh:21
Finding
Path Traversal in Campaign, Template, and Lead-Set Identifiers<![CDATA[ ## Vulnerability Details **File Location**: `scripts/create-campaign.sh:21-23, 40-44, 83-85`; `scripts/send-campaign.sh:18-20, 40, 139-141` **Vulnerability Type**: Path traversal causing unintended JSON file access and overwrite **Risk Level**: Medium ### Vulnerable Code From `scripts/create-campaign.sh`: ```bash BASE_DIR="${LEAD_GEN_DIR:-$HOME/.openclaw/workspace/lead-gen}" CAMPAIGN_DIR="$BASE_DIR/campaigns" TEMPLATE_FILE="$BASE_DIR/templates/$TEMPLATE.json" LEADS_DIR="$BASE_DIR/leads/$LEADS" ``` ```python name = os.environ["CAMP_NAME"] base_dir = os.environ["CAMP_BASE_DIR"] template_path = os.environ["CAMP_TEMPLATE"] leads_dir = os.environ["CAMP_LEADS_DIR"] campaign_dir = os.environ["CAMP_DIR"] with open(template_path) as f: template = json.load(f) ``` ```python output = os.path.join(campaign_dir, f"{name}.json") with open(output, "w") as f: json.dump(campaign, f, indent=2) ``` From `scripts/send-campaign.sh`: ```bash BASE_DIR="${LEAD_GEN_DIR:-$HOME/.openclaw/workspace/lead-gen}" CAMPAIGN_FILE="$BASE_DIR/campaigns/$CAMPAIGN.json" [ ! -f "$CAMPAIGN_FILE" ] && { echo "❌ Campaign not found: $CAMPAIGN"; exit 1; } ``` ```python with open(campaign_file) as f: campaign = json.load(f) ``` ```python with open(campaign_file, "w") as f: json.dump(campaign, f, indent=2) ``` ### Technical Analysis The `NAME`, `TEMPLATE`, `LEADS`, and `CAMPAIGN` arguments are incorporated into filesystem paths without validation or canonical containment checks. Values containing `..` or path separators can escape the intended `campaigns`, `templates`, or `leads` directories. Campaign creation writes to the resulting path with truncation, allowing an existing JSON file to be replaced. Campaign sending reads a traversed JSON file and later rewrites it if its structure is compatible with the expected campaign format. Symbolic links are not rejected, creating another route to targets outside the intended directory. ### Attack Path 1. An attacker or un ...[truncated 997 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Restrict all identifiers to a conservative format such as `^[A-Za-z0-9_-]+$`. - Resolve every constructed path with `Path.resolve()` and verify that it remains below the intended base directory using `Path.is_relative_to()` or an equivalent containment check. - Reject absolute paths, `..`, path separators, NUL characters, and platform-specific path prefixes. - Refuse to follow symbolic links when opening campaign, template, and lead files. - Use exclusive file creation for new campaigns and require an explicit overwrite option. - Validate `--leads` against an allowlist such as `qualified` and `enriched`. - Apply the same validation consistently in both campaign creation and campaign sending. ]]>

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/send-campaign.sh:69
Finding
Documented Human Approval and Email Compliance Controls Are Not Enforced<![CDATA[ ## Vulnerability Details **File Location**: `scripts/send-campaign.sh:69-82, 88-137`; `scripts/init-workspace.sh:45-66`; `SKILL.md:126-134, 168-174` **Vulnerability Type**: Missing enforcement of safety and compliance controls **Risk Level**: Medium ### Vulnerable Code The dry-run path only previews content and exits: ```python if dry_run: print(f"🔍 DRY RUN — Campaign: {campaign['name']}") print(f" Recipients: {len(pending)} pending / {len(recipients)} total") print() for r in pending[:5]: subject = template["subject"].replace("{company_name}", r.get("company_name", "")) body = template["body"] for key in ["first_name", "last_name", "company_name", "domain"]: body = body.replace(f"{{{key}}}", r.get(key, f"[{key}]")) print(f" To: {r['email']}") print(f" Subject: {subject}") print(f" Body preview: {body[:150]}...") print() print(" ⚠️ Remove --dry-run to send for real") sys.exit(0) ``` If `--dry-run` is omitted, sending starts without checking an approval record: ```python print(f"📧 Sending campaign: {campaign['name']}") print(f" {len(pending)} emails to send (limit: {daily_limit}/day)") print() sent = 0 for r in pending: if sent >= daily_limit: print(f"\n ⚠️ Daily limit reached ({daily_limit}). Remaining will send tomorrow.") break subject = template["subject"] body = template["body"] for key in ["first_name", "last_name", "company_name", "domain"]: subject = subject.replace(f"{{{key}}}", r.get(key, "")) body = body.replace(f"{{{key}}}", r.get(key, "")) resp = requests.post( "https://api.sendgrid.com/v3/mail/send", headers={ "Authorization": f"Bearer {sendgrid_key}", "Content-Type": "application/json", }, json={ "personalizations": [{"to": [{"email": r["email"]}]}], "from": {"email": from_email, "name": fr ...[truncated 2358 chars]
Remediation
<![CDATA[ ## Remediation Suggestions - Introduce an explicit campaign state machine such as `draft → previewed → approved → active`. - Record a hash of the exact recipient list, template, sender, and rendered content during preview. - Require a separate approval operation that records the approving user, timestamp, and reviewed content hash. - Refuse to send if the campaign changed after approval or if no valid approval record exists. - Require an explicit confirmation token rather than treating omission of `--dry-run` as authorization. - Validate every template for a functional opt-out mechanism and required sender identity and postal-address content before approval. - Maintain and enforce a suppression list for opt-outs, bounces, and recipients who request removal. - Enforce a non-bypassable upper daily limit and require separate approval to raise it. - Validate recipient addresses and provide a complete preview or export for review rather than previewing only the first five recipients. ]]>
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Trigger AbuseOverly Broad Trigger, Shadow Command Trigger, Keyword Baiting Trigger
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (24)

Tp4

High
Category
MCP Tool Poisoning
Confidence
94% confidence
Finding
If the skill description promises broad CRM and outreach automation while actual behavior is narrower and includes outbound email via SendGrid without clearly declared permissions, users may approve or invoke the skill under false assumptions. That can lead to unauthorized external transmission of lead/contact data or email sending beyond what the manifest transparently discloses.

Tp4

High
Category
MCP Tool Poisoning
Confidence
95% confidence
Finding
If the skill description promises broad CRM and outreach automation while actual behavior is narrower and includes outbound email via SendGrid without clearly declared permissions, users may approve or invoke the skill under false assumptions. That can lead to unauthorized external transmission of lead/contact data or email sending beyond what the manifest transparently discloses.

Credential Access

High
Category
Privilege Escalation
Content
### Get API Key
1. Go to Settings → Integrations → Private Apps
2. Create a private app with scopes: `crm.objects.contacts.write`, `crm.objects.deals.write`
3. Copy the access token

### Configure
```json
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Credential Access

High
Category
Privilege Escalation
Content
## Zoho CRM

### Get Access Token
1. Go to https://api-console.zoho.com/
2. Create a Self Client
3. Generate token with scope: `ZohoCRM.modules.leads.CREATE,ZohoCRM.modules.contacts.CREATE`
Confidence
70% confidence
Finding
Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Lp3

Medium
Category
MCP Least Privilege
Confidence
89% confidence
Finding
The skill advertises workflows that inherently require network, filesystem, and likely secret access, but it does not declare an explicit tool scope or permission boundary. That makes it easier for an agent runtime to over-grant capabilities or for users to misunderstand the operational reach of the skill, increasing the chance of unintended data access or outbound actions.

Vague Triggers

Medium
Confidence
95% confidence
Finding
The activation guidance is extremely broad and overlaps with many normal sales and CRM tasks, which raises the risk that the skill is invoked in situations involving sensitive customer or prospect data without deliberate user intent. In a skill that can scrape, enrich, sync to CRMs, and send outreach, ambiguous triggering materially increases the chance of privacy-impacting or externally visible actions.

Missing User Warnings

Medium
Confidence
93% confidence
Finding
The skill handles collection, enrichment, storage, and third-party transmission of personal and business contact data, yet the documentation gives only cursory compliance notes and does not clearly explain consent, lawful basis, retention, or third-party sharing risks. In this context, missing privacy warnings are significant because the workflows directly encourage scraping, enrichment, CRM upload, and outreach at scale.

Missing User Warnings

Medium
Confidence
95% confidence
Finding
The guide shows users placing live CRM API credentials directly into a JSON configuration example without any warning about secret handling, storage, rotation, or avoiding commits to source control. In an automation skill that orchestrates CRM and outreach actions, this increases the chance users will hardcode reusable tokens in files that may be logged, shared, or checked into repositories, leading to unauthorized CRM access.

Missing User Warnings

Medium
Confidence
89% confidence
Finding
The CSV fallback documents that lead data will be exported to a local file but provides no warning that the file may contain sensitive personal or business contact data. In a lead-generation/CRM context, writing prospect data to disk without retention, access-control, or cleanup guidance can cause unintended disclosure through shared workspaces, backups, or accidental attachment/sharing.

External Transmission

Medium
Category
Data Exfiltration
Content
try:
    resp = requests.get(
        "https://api.search.brave.com/res/v1/web/search",
        headers={"X-Subscription-Token": brave_key},
        params={"q": search_query, "count": min(count, 20)},
        timeout=15,
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
94% confidence
Finding
The script sends the lead's domain to Hunter.io and later fetches the company's website for enrichment, which transmits user-managed lead data over the network. Although there are progress prints, there is no explicit disclosure, confirmation, or comment warning that external services will receive lead information.

External Transmission

Medium
Category
Data Exfiltration
Content
if hunter_key and domain:
        try:
            resp = requests.get(
                "https://api.hunter.io/v2/domain-search",
                params={"domain": domain, "api_key": hunter_key},
                timeout=10,
            )
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Dynamic Request Target

Medium
Category
Server-Side Request Forgery
Content
# Basic web scrape for additional signals
    if domain:
        try:
            resp = requests.get(f"https://{domain}", timeout=8, headers={"User-Agent": "ReighlanLeadBot/1.0"})
            if resp.status_code == 200:
                text = resp.text.lower()
                # Tech signals
Confidence
96% confidence
Finding
The script builds a request target directly from lead-controlled `domain` data and performs a server-side fetch without validation. An attacker who can supply or modify lead JSON could cause requests to arbitrary hosts, including internal services or cloud metadata endpoints via DNS rebinding or crafted hostnames, turning the agent into an SSRF primitive.

Missing User Warnings

Medium
Confidence
97% confidence
Finding
After writing the enriched record, the script removes the original file from the raw directory. This is an irreversible file operation, but there is no confirmation prompt or explicit warning that running the script will delete source lead files.

Missing User Warnings

Medium
Confidence
90% confidence
Finding
This shell script writes a config.json file that includes fields for API keys, access tokens, refresh tokens, and SMTP credentials. Although it logs that the file was created, it does not warn the user that sensitive secrets will be stored in this workspace path, which is a user-disclosure gap for credential-related file creation.

External Transmission

Medium
Category
Data Exfiltration
Content
# Create contact
        try:
            resp = requests.post(
                "https://api.hubapi.com/crm/v3/objects/contacts",
                headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
                json={
Confidence
70% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
# Create contact
        try:
            resp = requests.post(
                "https://api.hubapi.com/crm/v3/objects/contacts",
                headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
                json={
Confidence
70% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
# Create contact
        try:
            resp = requests.post(
                "https://api.hubapi.com/crm/v3/objects/contacts",
                headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
                json={
Confidence
80% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
# Create contact
        try:
            resp = requests.post(
                "https://api.hubapi.com/crm/v3/objects/contacts",
                headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"},
                json={
                    "properties": {
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
return False
        
        try:
            resp = requests.post(
                f"https://{domain}.pipedrive.com/api/v1/persons",
                params={"api_token": api_key},
                json={
Confidence
80% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Dynamic Request Target

Medium
Category
Server-Side Request Forgery
Content
return False
        
        try:
            resp = requests.post(
                f"https://{domain}.pipedrive.com/api/v1/persons",
                params={"api_token": api_key},
                json={
Confidence
93% confidence
Finding
The Pipedrive request target is constructed from a configurable domain value without strict validation: `https://{domain}.pipedrive.com/api/v1/persons`. If an attacker can influence config or environment variables, they may redirect requests to attacker-controlled or unintended subdomains, causing unauthorized transmission of lead data and API usage against the wrong tenant. In a skill that automates handling of large volumes of lead/contact data, this increases the risk of silent data leakage at scale.

Missing User Warnings

Medium
Confidence
86% confidence
Finding
The csv branch appends lead contact data to reports/leads-export.csv, which is a file write involving user/business data. While network pushes are part of the script's stated purpose and are visibly announced, this local export path has no specific warning, confirmation, or explanatory comment/disclosure near the operation.

External Transmission

Medium
Category
Data Exfiltration
Content
body = body.replace(f"{{{key}}}", r.get(key, ""))
    
    try:
        resp = requests.post(
            "https://api.sendgrid.com/v3/mail/send",
            headers={
                "Authorization": f"Bearer {sendgrid_key}",
Confidence
80% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

External Transmission

Medium
Category
Data Exfiltration
Content
try:
        resp = requests.post(
            "https://api.sendgrid.com/v3/mail/send",
            headers={
                "Authorization": f"Bearer {sendgrid_key}",
                "Content-Type": "application/json",
Confidence
60% confidence
Finding
Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.