Back to skill

Security audit

CRM Snail Mail (PostGrid)

Security checks for vulnerabilities and agentic risk

Overview

The skill can perform the stated CRM-to-postal-mail workflow, but it also gives agents broad PostGrid API power that goes well beyond sending letters or postcards.

Treat this as a high-privilege PostGrid administration utility, not just a CRM mailer. Install only if you are comfortable giving the agent access to CRM contacts, customer postal data, and a PostGrid key; use least-privilege or test credentials, avoid base URL overrides, start with dry-run and small max-send values, and review payloads before any live send.

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

T09 · Insecure Skill Coding Practices

Error
Location
scripts/crm_postgrid_mailer.py:217
Finding

Arbitrary CRM Base URLs Receive GHL or FUB Credentials

Content
View full analysis
List[Dict[str, Any]]: api_key = args.api_key or os.getenv("GHL_API_KEY") if not api_key: raise MailerError("Missing GHL API key. Set GHL_API_KEY or pass --api-key.") base_url = (args.base_url or os.getenv("GHL_BASE_URL") or DEFAULT_GHL_BASE_URL).rstrip("/") query: Dict[str, Any] = {"limit": args.limit} if args.location_id: query["locationId"] = args.location_id url = f"{base_url}/contacts/?{urllib.parse.urlencode(query)}" headers = { "Accept": "application/json", "Authorization": f"Bearer {api_key}", "Version": args.ghl_version, } payload = _http_json("GET", url, headers=headers, timeout=args.timeout) contacts = _extract_ghl_contacts(payload) return [_normalize_contact(item, "ghl") for item in contacts] def fetch_contacts_from_fub(args: argparse.Namespace) -> List[Dict[str, Any]]: api_key = args.api_key or os.getenv("FUB_API_KEY") if not api_key: raise MailerError("Missing FUB API key. Set FUB_API_KEY or pass --api-key.") base_url = (args.base_url or os.getenv("FUB_BASE_URL") or DEFAULT_FUB_BASE_URL).rstrip("/") auth_header = base64.b64encode(f"{api_key}:".encode("utf-8")).decode("ascii") query = {"limit": args.limit} url = f"{base_url}/people?{urllib.parse.urlencode(query)}" headers = { "Accept": "application/json", "Authorization": f"Basic {auth_header}", } payload = _http_json("GET", url, headers=headers, timeout=args.timeout) people = _extract_fub_people(payload) return [_normalize_contact(item, "fub") for item in people] ``` The desti ...[truncated 1915 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/crm_postgrid_mailer.py:371
Finding

Configurable PostGrid Destination Can Receive API Keys and Customer PII

Content
View full analysis
Dict[str, Any]: base = base_url.rstrip("/") clean_route = route.strip().lstrip("/") url = f"{base}/{clean_route}" headers = { "Accept": "application/json", api_key_header: api_key, } return _http_json("POST", url, headers=headers, body=payload, timeout=timeout) ``` The send commands expose both the destination and credential-header name: ```python parser.add_argument( "--postgrid-base-url", default=DEFAULT_POSTGRID_BASE_URL, help=f"PostGrid API base URL (default: {DEFAULT_POSTGRID_BASE_URL})", ) parser.add_argument("--postgrid-key-header", default="x-api-key", help="PostGrid API key header name") ``` ### Technical Analysis `_postgrid_send` unconditionally attaches the PostGrid API key to a request targeting `base_url`. The value is caller-controlled through `--postgrid-base-url`, and the code does not enforce HTTPS or verify that the destination is owned by PostGrid. The request body contains sensitive mailing information, including recipient name and postal address, sender details, rendered letter content, and merge variables. Consequently, an unsafe destination override exposes both the account credential and customer PII. Changing the credential-header name is not inherently dangerous, but it increases flexibility for disguising credential forwarding when combined with an unrestricted destination. ### Attack Path 1. An attacker supplies or induces an Agent to use `--postgrid-b ...[truncated 823 chars]
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Error
Location
scripts/postgrid_api.py:186
Finding

Raw API Caller Forwards PostGrid Credentials to Arbitrary Origins

Content
View full analysis
Dict[str, Any]: base = base_url.rstrip("/") url = base + path if query: clean = {k: v for k, v in query.items() if v is not None} if clean: url += "?" + urllib.parse.urlencode(clean, doseq=True) headers: Dict[str, str] = { "Accept": "application/json", key_header: api_key, } data = None if body is not None: headers["Content-Type"] = "application/json" data = json.dumps(body).encode("utf-8") req = urllib.request.Request(url=url, method=method.upper(), headers=headers, data=data) try: with urllib.request.urlopen(req, timeout=timeout) as resp: raw = resp.read().decode("utf-8") return json.loads(raw) if raw else {} ``` ```python def cmd_call_raw(args: argparse.Namespace) -> int: try: api_key = _resolve_api_key(args) query = parse_kv_pairs(args.query) body = _load_body(args) base_url = args.base_url path = args.path if args.path.startswith("/") else "/" + args.path response = _request( method=args.method, base_url=base_url, path=path, api_key=api_key, query=query, body=body, timeout=args.timeout, key_header=args.key_header, ) print(json.dumps(response, indent=2)) return 0 ``` ```python p_raw = sub.add_parser("call-raw", help="Call any PostGrid endpoint path ...[truncated 1841 chars]
Remediation
View remediation

T05 · Unauthorized Access and Privilege Escalation

Warning
Location
scripts/postgrid_api.py:34
Finding

PostGrid Utility Exposes Destructive and Financial Operations Beyond the Mailing Workflow

Content
View full analysis
Remediation
View remediation

T09 · Insecure Skill Coding Practices

Note
Location
scripts/crm_postgrid_mailer.py:319
Finding

Unnecessary CRM Attributes Are Sent to PostGrid and Printed in Dry-Run Output

Content
View full analysis
Dict[str, Any]: payload: Dict[str, Any] = { "to": _build_to_object(contact), "from": sender, "description": description, "mergeVariables": { "id": contact.get("id"), "first_name": contact.get("first_name"), "last_name": contact.get("last_name"), "full_name": contact.get("full_name"), "email": contact.get("email"), "phone": contact.get("phone"), "city": contact.get("city"), "state": contact.get("state"), "postal_code": contact.get("postal_code"), }, } ``` Dry-run mode stores the complete payload in the summary: ```python if args.dry_run: sent += 1 results.append({ "contact_id": contact_id, "status": "dry_run", "payload": payload, }) continue ``` The complete summary is printed and may also be written to disk: ```python if args.output: _write_json(args.output, summary) print(json.dumps(summary, indent=2)) ``` ### Technical Analysis Email addresses and phone numbers are included as merge variables in every PostGrid payload, even though postal delivery itself only requires recipient identity and mailing-address information. Unless a selected template actually references those fields, their disclosure is unnecessary. In dry-run mode, the full payload—including recipient address, sender address, email, phone number, and rendered content—is placed in the result and prin ...[truncated 1177 chars]
Remediation
View remediation
Vulnerability Patterns
  • Data ExfiltrationExternal Transmission, Env Variable Harvesting, File System Enumeration
  • Excessive AgencyUnrestricted Tool Access, Autonomous Decision Making, Scope Creep
  • MCP Least PrivilegeUnderdeclared Capability, Wildcard Permission, Missing Permission Declaration
  • MCP Tool PoisoningHidden Instructions, Unicode Deception, Parameter Description Injection
  • Prompt InjectionInstruction Override, Hidden Instructions, Exfiltration Commands
Findings (22)

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding

The skill is presented as a CRM-to-PostGrid mailer, but it also advertises a broad API utility with raw endpoint invocation and access to administrative, financial, webhook, and mailbox operations. This is dangerous because it turns a narrowly described workflow skill into a generic privileged API client, enabling actions far beyond user expectation and increasing the blast radius of credential misuse.

Content

No source excerpt is available for this finding.

Tp4

High
Category
MCP Tool Poisoning
Confidence
97% confidence
Finding

The skill is presented as a CRM-to-PostGrid mailer, but it also advertises a broad API utility with raw endpoint invocation and access to administrative, financial, webhook, and mailbox operations. This is dangerous because it turns a narrowly described workflow skill into a generic privileged API client, enabling actions far beyond user expectation and increasing the blast radius of credential misuse.

Content

No source excerpt is available for this finding.

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
- `scripts/postgrid_api.py`

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
- `scripts/postgrid_api.py`

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
- `scripts/postgrid_api.py`

Ae1

High
Category
analysis-evasion
Confidence
100% confidence
Finding

Referenced artifact was not completely inspected

Content

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

md
- `scripts/postgrid_api.py`

Description-Behavior Mismatch

High
Category
Not specified by scanner
Confidence
98% confidence
Finding

The module is scoped far beyond the stated CRM snail-mail use case: it includes administrative, financial, webhook, sub-organization, campaign, and virtual mailbox operations. In an agent skill context, this violates least privilege and creates unnecessary capability that could be abused to enumerate data, modify configuration, create financial artifacts, or perform destructive actions with the configured API key.

Content

No source excerpt is available for this finding.

Context-Inappropriate Capability

High
Category
Not specified by scanner
Confidence
99% confidence
Finding

The raw caller lets the skill send authenticated requests to arbitrary PostGrid paths, bypassing the curated endpoint catalog entirely. That effectively grants blanket API capability to the agent, enabling access to undocumented or unintended operations and undermining any safety assumptions based on the skill's advertised purpose.

Content

No source excerpt is available for this finding.

Undeclared Tool Scope

Medium
Category
MCP Least Privilege
Confidence
92% confidence
Finding

The skill exposes capabilities involving environment access, file I/O, and network calls, but does not declare any tool scope restrictions such as permissions or allowed-tools. That creates an authorization and review gap: consumers may assume a narrower capability set than the skill actually exercises, increasing the risk of unintended data access, outbound transmission, or misuse of credentials.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
95% confidence
Finding

The documented call-raw command permits arbitrary direct invocation of PostGrid endpoints, which goes beyond normal mail submission and can be used to access or modify unrelated resources depending on API key privileges. In context, this is more dangerous because the skill is framed as a targeted CRM mailing tool, so operators may not expect generic API execution capability with broad outbound request control.

Content

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

bash
python3 scripts/postgrid_api.py call-raw GET /letters \
  --base-url https://api.postgrid.com/print-mail/v1

Normalize contacts from FUB to JSON:

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% 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 31)May include surrounding context.

md
from typing import Any, Dict, List, Optional, Sequence

DEFAULT_GHL_BASE_URL = "https://services.leadconnectorhq.com"
DEFAULT_FUB_BASE_URL = "https://api.followupboss.com/v1"
DEFAULT_POSTGRID_BASE_URL = "https://api.postgrid.com/print-mail/v1"

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% 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/crm_postgrid_mailer.py (reported line 20)May include surrounding context.

python
from typing import Any, Dict, List, Optional, Sequence

DEFAULT_GHL_BASE_URL = "https://services.leadconnectorhq.com"
DEFAULT_FUB_BASE_URL = "https://api.followupboss.com/v1"
DEFAULT_POSTGRID_BASE_URL = "https://api.postgrid.com/print-mail/v1"

External Transmission

Medium
Category
Data Exfiltration
Confidence
83% confidence
Finding

The hardcoded PostGrid endpoint is used for live transmission of recipient PII to a third-party print-and-mail service. In the context of a CRM direct-mail skill, this external transmission is expected, but it is still security-relevant because it exports names and postal addresses outside the source CRM and can be triggered without an explicit confirmation safeguard.

Content

Scanner excerpt · scripts/crm_postgrid_mailer.py (reported line 21)May include surrounding context.

python
DEFAULT_GHL_BASE_URL = "https://services.leadconnectorhq.com"
DEFAULT_FUB_BASE_URL = "https://api.followupboss.com/v1"
DEFAULT_POSTGRID_BASE_URL = "https://api.postgrid.com/print-mail/v1"


class MailerError(RuntimeError):

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
95% confidence
Finding

The send and one-off code paths transmit recipient names, addresses, and related contact data to the external PostGrid API without any explicit interactive confirmation, consent check, or prominent runtime disclosure. In a CRM automation skill handling customer PII, this creates a real privacy and data-governance risk because a user can trigger bulk physical-mail sending and external sharing of sensitive address data with little friction.

Content

No source excerpt is available for this finding.

Intent-Code Divergence

Medium
Category
Not specified by scanner
Confidence
94% confidence
Finding

The docstring explicitly advertises support for 'the full docs surface' and 'any documented endpoint,' including partner/admin resources, which is inconsistent with a narrowly described CRM mail-sending skill. In practice this signals intentional overbroad capability and increases the chance the agent will use powerful endpoints unrelated to user intent.

Content

No source excerpt is available for this finding.

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% 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 33)May include surrounding context.

md
from dataclasses import dataclass
from typing import Any, Dict, List, Optional

DEFAULT_PRINT_MAIL_BASE_URL = "https://api.postgrid.com/print-mail/v1"
DEFAULT_CORE_BASE_URL = "https://api.postgrid.com/v1"

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% 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 · references/postgrid-notes.md (reported line 5)May include surrounding context.

md
from dataclasses import dataclass
from typing import Any, Dict, List, Optional

DEFAULT_PRINT_MAIL_BASE_URL = "https://api.postgrid.com/print-mail/v1"
DEFAULT_CORE_BASE_URL = "https://api.postgrid.com/v1"

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% 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/postgrid_api.py (reported line 22)May include surrounding context.

python
from dataclasses import dataclass
from typing import Any, Dict, List, Optional

DEFAULT_PRINT_MAIL_BASE_URL = "https://api.postgrid.com/print-mail/v1"
DEFAULT_CORE_BASE_URL = "https://api.postgrid.com/v1"

External Transmission

Medium
Category
Data Exfiltration
Confidence
60% 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/postgrid_api.py (reported line 23)May include surrounding context.

python
from dataclasses import dataclass
from typing import Any, Dict, List, Optional

DEFAULT_PRINT_MAIL_BASE_URL = "https://api.postgrid.com/print-mail/v1"
DEFAULT_CORE_BASE_URL = "https://api.postgrid.com/v1"

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
91% confidence
Finding

The endpoint catalog includes multiple DELETE and cancellation operations such as deleting contacts, templates, webhooks, campaigns, and canceling letters, postcards, cheques, and snap packs. Although these actions are part of the utility's purpose, the file provides no visible warning, confirmation step, or cautionary help text before invoking irreversible or potentially destructive operations.

Content

No source excerpt is available for this finding.

Missing User Warnings

Medium
Category
Not specified by scanner
Confidence
99% confidence
Finding

The raw-call interface accepts a user-supplied base URL and then sends the PostGrid API key in the configured header to that destination. This can leak credentials to arbitrary external hosts, turning the tool into an authenticated SSRF/credential-exfiltration primitive rather than a bounded PostGrid client.

Content

No source excerpt is available for this finding.

Missing User Warnings

Low
Category
Not specified by scanner
Confidence
78% confidence
Finding

The GHL and FUB fetch functions make authenticated HTTP requests to pull contact records that may include names, emails, phone numbers, and addresses. The code lacks a user-facing notice or inline warning that running these commands accesses external CRM data and may export it to local JSON files.

Content

No source excerpt is available for this finding.

Static analysis

No suspicious patterns detected.