Back to skill

Security audit

Delete No Email Contacts

Security checks for vulnerabilities and agentic risk

Overview

This skill appears to do the advertised HubSpot cleanup, but it deletes CRM contacts and leaves local CSV audit files that users should handle carefully.

Install only if you intend to let this skill delete HubSpot contacts that lack email addresses. Use a least-privileged HubSpot private app token limited to contacts read/write, review the before-state CSV and count before typing DELETE, confirm the safety threshold fits your expected cleanup size, and keep generated CSV files out of source control, backups, and shared folders unless protected.

Vulnerability Patterns
  • Insecure DependenciesIntroduces malicious components through unsafe dependency sources
  • 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 (2)

T09 · Insecure Skill Coding Practices

Warning
Location
scripts/before.py:58
Finding

Persistent Plaintext Export of HubSpot CRM Personal Data

Content
View full analysis
Remediation
View remediation

T08 · Insecure Dependencies

Note
Location
scripts/before.py:1
Finding

Dependencies Are Not Locked to Audited Versions

Content
View full analysis
=3.10" # dependencies = [ # "requests>=2.31", # "python-dotenv>=1.0", # ] # /// ``` The installation instructions also resolve unspecified current versions: ```bash uv init hubspot-cleanup cd hubspot-cleanup uv add requests python-dotenv ``` ### Technical Analysis The scripts permit any future version of `requests` at or above 2.31 and any future version of `python-dotenv` at or above 1.0. The project does not include a reviewed lockfile or package integrity hashes. The package names are legitimate and there is no evidence of dependency confusion, typosquatting, or an intentionally malicious package. Nevertheless, future executions may install dependency versions that were not included in this audit. This makes the effective executable code dependent on mutable package-registry state. The scripts process a HubSpot private-app token and perform CRM write operations. A compromised dependency release, compromised package account, package-index redirection, or malicious dependency source could execute code during installation or import in a context where the token is available. Broad lower-bound constraints increase the gap between the reviewed source and the code actually executed. ### Attack Path 1. An operator follows the setup instructions or runs a dependency-aware script without a frozen environment. 2. `uv` resolves dependency versions available at execution time rather than versions fixed during review. 3. A package source supplies a compromised or malicious version satisfying `requests>=2.31` or `python-dotenv>=1.0`, or one of their transitive dependen ...[truncated 1196 chars]
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Privilege EscalationExcessive Permissions, Sudo/Root Execution, Credential Access
  • Taint TrackingDirect Taint Flow, Variable-Mediated Taint Flow, Credential Exfiltration Chain
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
Findings (24)

Tainted flow: 'headers' from os.environ (line 24, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/after.py (reported line 51)May include surrounding context.

python
print("=" * 60)
print()

response = requests.post(url, headers=headers, json=search_payload)
response.raise_for_status()

data = response.json()

Tainted flow: 'headers' from os.environ (line 25, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/before.py (reported line 61)May include surrounding context.

python
print(f"  Filter: email NOT_HAS_PROPERTY")
print()

response = requests.post(url, headers=headers, json=search_payload)
response.raise_for_status()

data = response.json()

Tainted flow: 'headers' from os.environ (line 25, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/before.py (reported line 97)May include surrounding context.

python
if after:
        payload["after"] = after

    resp = requests.post(url, headers=headers, json=payload)
    if resp.status_code != 200:
        print(f"  Stopped at {len(all_contacts)} (status {resp.status_code})")
        break

Tainted flow: 'headers' from os.environ (line 24, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/execute.py (reported line 67)May include surrounding context.

python
payload["after"] = after

    url = f"{BASE}/crm/v3/objects/contacts/search"
    resp = requests.post(url, headers=headers, json=payload)
    resp.raise_for_status()
    data = resp.json()

Tainted flow: 'headers' from os.environ (line 24, credential/environment) → requests.post (network output)

Critical
Category
Data Flow
Confidence
90% confidence
Finding

Credentials or environment variables flow to a network sink. This is a high-confidence indicator of credential exfiltration.

Content

Scanner excerpt · scripts/execute.py (reported line 122)May include surrounding context.

python
print(f"  Batch {batch_num}/{total_batches}: deleting {len(batch)} contacts...", end=" ")

    resp = requests.post(delete_url, headers=headers, json=delete_payload)

    if resp.status_code == 204:
        deleted_count += len(batch)

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

This code chunk is an after-state verification script, not the deletion automation described. It searches for contacts without email and reports whether any remain, optionally comparing the result to a local CSV file. There is no call to a HubSpot batch archive or delete endpoint, so the primary behavior materially differs from the declared purpose of deleting contacts. The HubSpot resource is related, but the capability in this chunk is audit/verification rather than deletion.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
98% confidence
Finding

The declared description says the skill deletes no-email contacts from HubSpot and is fully automated via Search and Batch Archive APIs. The supplied code chunk only performs discovery and reporting: it queries contacts missing email addresses, collects their metadata, saves the results to a CSV, and prints lifecycle/source summaries. There is no deletion or archive API call in this chunk, and the script explicitly says the next step is to review the CSV and then run another script to delete. That makes the actual behavior materially different from the declared primary purpose, with an undeclared data export capability as well.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding

The core purpose matches: the code targets HubSpot contacts with no email address and deletes them via the CRM search and batch archive endpoints. However, the description says the process is 'Fully automated,' while the script pauses for manual confirmation by requiring the user to type 'DELETE' before any deletion occurs. That is a meaningful behavior mismatch. Additionally, the script stores contact IDs and deletion statuses in a local CSV file, which is an extra data export/logging behavior not mentioned in the description. This logging is somewhat ancillary, but it still represents undeclared handling of CRM data. Resources accessed remain consistent with HubSpot plus local filesystem for logging, and there are no unrelated triggers.

Content

No source excerpt is available for this finding.

Credential Access

High
Category
Privilege Escalation
Confidence
70% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · SKILL.md (reported line 22)May include surrounding context.

md
## Prerequisites

- A HubSpot private app access token with `crm.objects.contacts.read` and `crm.objects.contacts.write` scopes
- Python 3.10+ with `uv` for package management
- A `.env` file containing `HUBSPOT_ACCESS_TOKEN`

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/after.py (reported line 19)May include surrounding context.

python
import requests
from dotenv import load_dotenv

load_dotenv(os.path.join(os.path.dirname(__file__), "..", ".env"))

TOKEN = os.environ["HUBSPOT_ACCESS_TOKEN"]
BASE = "https://api.hubapi.com"

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/before.py (reported line 20)May include surrounding context.

python
import requests
from dotenv import load_dotenv

load_dotenv(os.path.join(os.path.dirname(__file__), "..", ".env"))

TOKEN = os.environ["HUBSPOT_ACCESS_TOKEN"]
BASE = "https://api.hubapi.com"

Credential Access

High
Category
Privilege Escalation
Confidence
60% confidence
Finding

Code accesses credential files (SSH keys, AWS credentials, etc.). This could indicate credential theft attempts.

Content

Scanner excerpt · scripts/execute.py (reported line 19)May include surrounding context.

python
import requests
from dotenv import load_dotenv

load_dotenv(os.path.join(os.path.dirname(__file__), "..", ".env"))

TOKEN = os.environ["HUBSPOT_ACCESS_TOKEN"]
BASE = "https://api.hubapi.com"

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
94% confidence
Finding

The skill executes code that reads environment secrets, writes local files, and makes network requests, but it does not declare any tool or permission scope. That omission weakens reviewability and least-privilege controls, making it easier for an agent runtime to grant broader capabilities than users expect for a destructive CRM cleanup task.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
70% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · SKILL.md (reported line 147)May include surrounding context.

md
if after:
        payload["after"] = after

    resp = requests.post(
        f"{BASE}/crm/v3/objects/contacts/search",
        headers=headers, json=payload,
    )

External Transmission

Medium
Category
Data Exfiltration
Confidence
70% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · SKILL.md (reported line 200)May include surrounding context.

md
batch = all_ids[i : i + BATCH_SIZE]
    delete_payload = {"inputs": [{"id": cid} for cid in batch]}

    resp = requests.post(
        f"{BASE}/crm/v3/objects/contacts/batch/archive",
        headers=headers, json=delete_payload,
    )

External Transmission

Medium
Category
Data Exfiltration
Confidence
80% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · SKILL.md (reported line 76)May include surrounding context.

md
print("=" * 60)
print()

response = requests.post(url, headers=headers, json=search_payload)
response.raise_for_status()

data = response.json()

External Transmission

Medium
Category
Data Exfiltration
Confidence
80% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · SKILL.md (reported line 232)May include surrounding context.

md
print("=" * 60)
print()

response = requests.post(url, headers=headers, json=search_payload)
response.raise_for_status()

data = response.json()

External Transmission

Medium
Category
Data Exfiltration
Confidence
80% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/after.py (reported line 51)May include surrounding context.

python
print("=" * 60)
print()

response = requests.post(url, headers=headers, json=search_payload)
response.raise_for_status()

data = response.json()

External Transmission

Medium
Category
Data Exfiltration
Confidence
80% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/before.py (reported line 61)May include surrounding context.

python
print("=" * 60)
print()

response = requests.post(url, headers=headers, json=search_payload)
response.raise_for_status()

data = response.json()

External Transmission

Medium
Category
Data Exfiltration
Confidence
80% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/before.py (reported line 97)May include surrounding context.

python
print("=" * 60)
print()

response = requests.post(url, headers=headers, json=search_payload)
response.raise_for_status()

data = response.json()

External Transmission

Medium
Category
Data Exfiltration
Confidence
80% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/execute.py (reported line 67)May include surrounding context.

python
print("=" * 60)
print()

response = requests.post(url, headers=headers, json=search_payload)
response.raise_for_status()

data = response.json()

Description-Behavior Mismatch

Medium
Category
Not specified by scanner
Confidence
98% confidence
Finding

The skill metadata says it deletes no-email contacts, but this file exports detailed CRM contact data to a local CSV instead. That mismatch increases risk because operators may run the skill expecting deletion-only behavior while it creates a local dataset containing personal and business metadata that can persist, be copied, or be mishandled.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
80% confidence
Finding

Data is being sent to an external URL. This could be legitimate telemetry or data exfiltration. Manual review is recommended.

Content

Scanner excerpt · scripts/execute.py (reported line 122)May include surrounding context.

python
print(f"  Batch {batch_num}/{total_batches}: deleting {len(batch)} contacts...", end=" ")

    resp = requests.post(delete_url, headers=headers, json=delete_payload)

    if resp.status_code == 204:
        deleted_count += len(batch)

Intent-Code Divergence

Low
Category
Not specified by scanner
Confidence
95% confidence
Finding

The docstring explicitly describes a 'Before State' script that counts contacts with no email and exports them to CSV for audit. This directly conflicts with the stated skill intent in the manifest, which frames the skill as fully automated deletion of such contacts.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.