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<![CDATA[ ## Vulnerability Details **File Location**: `scripts/crm_postgrid_mailer.py:217-253`, with the unrestricted CLI option at `scripts/crm_postgrid_mailer.py:620-621` **Vulnerability Type**: Credential disclosure through an unvalidated network destination **Risk Level**: High ### Vulnerable Code ```python def fetch_contacts_from_ghl(args: argparse.Namespace) -> 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
<![CDATA[ ## Remediation Suggestions 1. Parse endpoint overrides with `urllib.parse.urlparse`. 2. Require HTTPS for every authenticated CRM request. 3. Allowlist the exact official CRM hostnames by default: - `services.leadconnectorhq.com` - `api.followupboss.com` 4. Reject URLs containing user information, unexpected ports, fragments, or non-HTTPS schemes. 5. If custom endpoints are operationally necessary, require an explicit development-only flag and a separately supplied test credential. 6. Do not automatically reuse `GHL_API_KEY` or `FUB_API_KEY` for an unapproved origin. 7. Log the validated destination before a request without logging the Authorization header. 8. Configure CRM tokens with read-only, least-privilege scopes wherever the provider supports them. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/crm_postgrid_mailer.py:371
Finding
Configurable PostGrid Destination Can Receive API Keys and Customer PII<![CDATA[ ## Vulnerability Details **File Location**: `scripts/crm_postgrid_mailer.py:371-384`, with configurable destination options at `scripts/crm_postgrid_mailer.py:637-641` and `scripts/crm_postgrid_mailer.py:672-676` **Vulnerability Type**: Credential and personal-data disclosure through an unvalidated network destination **Risk Level**: High ### Vulnerable Code ```python def _postgrid_send( payload: Dict[str, Any], route: str, base_url: str, api_key: str, api_key_header: str, timeout: int, ) -> 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
<![CDATA[ ## Remediation Suggestions 1. Restrict live authenticated sends to the exact `https://api.postgrid.com` origin. 2. Validate the parsed scheme, hostname, and port before adding the API-key header. 3. Separate custom-endpoint testing from production operation. 4. Never automatically forward `POSTGRID_API_KEY` to a custom endpoint; require a distinct test credential. 5. Remove the configurable key-header option unless a documented PostGrid deployment requires it. 6. Display the validated destination and require explicit confirmation before live bulk sends. 7. Use least-privilege PostGrid keys and separate test and production credentials. 8. Minimize the payload to the fields required for printing and delivery. ]]>

T09 · Insecure Skill Coding Practices

Error
Location
scripts/postgrid_api.py:186
Finding
Raw API Caller Forwards PostGrid Credentials to Arbitrary Origins<![CDATA[ ## Vulnerability Details **File Location**: `scripts/postgrid_api.py:186-217`, `scripts/postgrid_api.py:293-312`, and `scripts/postgrid_api.py:339-345` **Vulnerability Type**: Authenticated arbitrary-origin request primitive **Risk Level**: High ### Vulnerable Code ```python def _request( method: str, base_url: str, path: str, api_key: str, query: Optional[Dict[str, str]] = None, body: Optional[Dict[str, Any]] = None, timeout: int = 30, key_header: str = "x-api-key", ) -> 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
<![CDATA[ ## Remediation Suggestions 1. Allow authenticated calls only to exact approved PostGrid HTTPS origins. 2. Validate both cataloged base URL overrides and `call-raw --base-url`. 3. Remove arbitrary-origin support from the authenticated raw caller. 4. If generic raw HTTP support is retained, do not resolve or attach `POSTGRID_API_KEY`; require a separately supplied credential. 5. Restrict methods and paths to documented PostGrid operations where possible. 6. Reject redirects to a different origin or ensure Authorization headers are never forwarded across origins. 7. Provide a safe request-preview mode that omits credentials and redacts sensitive body fields. 8. Use narrowly scoped API keys for utility operations. ]]>

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<![CDATA[ ## Vulnerability Details **File Location**: `scripts/postgrid_api.py:34-167`, including sensitive endpoint definitions at `scripts/postgrid_api.py:64-89` and `scripts/postgrid_api.py:130-151` **Vulnerability Type**: Excessive operational privileges and missing safeguards **Risk Level**: Medium ### Vulnerable Code ```python # Bank Accounts "bank_accounts.create": Endpoint("bank_accounts.create", "POST", "/bank_accounts", "bank_accounts", "Create bank account"), "bank_accounts.get": Endpoint("bank_accounts.get", "GET", "/bank_accounts/{id}", "bank_accounts", "Get bank account"), "bank_accounts.list": Endpoint("bank_accounts.list", "GET", "/bank_accounts", "bank_accounts", "List bank accounts"), "bank_accounts.delete": Endpoint("bank_accounts.delete", "DELETE", "/bank_accounts/{id}", "bank_accounts", "Delete bank account"), # Cheques / Checks "cheques.create": Endpoint("cheques.create", "POST", "/cheques", "cheques", "Create cheque"), "cheques.get": Endpoint("cheques.get", "GET", "/cheques/{id}", "cheques", "Get cheque"), "cheques.list": Endpoint("cheques.list", "GET", "/cheques", "cheques", "List cheques"), "cheques.cancel": Endpoint("cheques.cancel", "DELETE", "/cheques/{id}", "cheques", "Cancel cheque"), "cheques.cancel_with_note": Endpoint("cheques.cancel_with_note", "POST", "/cheques/{id}/cancel", "cheques", "Cancel cheque with note"), "cheques.progress_test": Endpoint("cheques.progress_test", "POST", "/cheques/{id}/progress", "cheques", "Progress test cheque"), "cheques.deposit_ready": Endpoint("cheques.deposit_ready", "GET", "/cheques/{id}/deposit_ready", "cheques", "Get deposit-ready e-check"), # Webhooks "webhooks.create": Endpoint("webhooks.create", "POST", "/webhooks", "webhooks", "Create webhook"), "webhooks.get": Endpoint("webhooks.get", "GET", "/webhooks/{id}", "webhooks", "Get webhook"), "webhooks.update": Endpoint("webhooks.update", "POST", "/webhooks/{id}", "webhooks", "Update webhook"), "webhooks.list": Endpoint("webhooks.list", "GE ...[truncated 2537 chars]
Remediation
<![CDATA[ ## Remediation Suggestions 1. Remove operations unrelated to contacts, letters, postcards, and required address verification from this Skill. 2. Move administrative, financial, and destructive operations into a separately reviewed utility. 3. Require separate least-privilege credentials for administrative and financial APIs. 4. Add explicit confirmation for all DELETE requests, campaign sends, cheque operations, bank-account operations, and webhook changes. 5. Provide a mandatory dry-run or request-preview stage for high-impact operations. 6. Implement a default-deny endpoint allowlist appropriate to the CRM mailing workflow. 7. Record an audit event containing the operation and target resource while redacting credentials and PII. 8. Document the minimum PostGrid API scopes needed by each command. ]]>

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<![CDATA[ ## Vulnerability Details **File Location**: `scripts/crm_postgrid_mailer.py:319-343`, with full payload output at `scripts/crm_postgrid_mailer.py:443-449` and `scripts/crm_postgrid_mailer.py:494-499` **Vulnerability Type**: Excessive personal-data disclosure **Risk Level**: Low ### Vulnerable Code ```python def _build_mail_payload( contact: Dict[str, Any], sender: Dict[str, Any], description: str, html_template: Optional[str], overrides: Optional[Dict[str, Any]], ) -> 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
<![CDATA[ ## Remediation Suggestions 1. Remove email and phone from default PostGrid merge variables. 2. Determine template variables actually referenced by the selected template and include only those values. 3. Redact recipient and sender PII from default dry-run summaries. 4. Provide a separate explicit option, such as `--show-full-payload`, when full inspection is necessary. 5. Mask postal addresses, email addresses, and phone numbers in console output. 6. Protect output files with restrictive permissions and document their sensitive-data content. 7. Avoid including complete API responses in persistent summaries unless required. 8. Define and enforce a retention policy for contact exports, dry-run results, and send summaries. ]]>
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.

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.

Ae1

High
Category
analysis-evasion
Content
- `scripts/postgrid_api.py`
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- `scripts/postgrid_api.py`
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- `scripts/postgrid_api.py`
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Ae1

High
Category
analysis-evasion
Content
- `scripts/postgrid_api.py`
Confidence
100% confidence
Finding
Referenced artifact was not completely inspected

Description-Behavior Mismatch

High
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.

Context-Inappropriate Capability

High
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.

Lp3

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.

External Transmission

Medium
Category
Data Exfiltration
Content
```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:
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.

External Transmission

Medium
Category
Data Exfiltration
Content
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"
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
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"
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
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):
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.

Missing User Warnings

Medium
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.

Intent-Code Divergence

Medium
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.

External Transmission

Medium
Category
Data Exfiltration
Content
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"
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
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"
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
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"
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
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"
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
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.

Missing User Warnings

Medium
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.

Missing User Warnings

Low
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.

Static analysis

No suspicious patterns detected.