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…
- Direction
- ingress
- Rego package
snowflake.ingress.guard_warehouse_export- App
- snowflake
- Bundles
- soc2pci-dssgdpr-ccpa
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:ab74fedbc153b6ae175a393a1be8bec7d675b0bd2e2d1272b5f1f3592785f94d
snowflakeguard-warehouse-sqlexportexfiltrationingresssoc2pci-dssgdpr-ccpa
What this policy does
Direction: ingress (tool_pre_invoke)
Default: deny on match, allow otherwise
Package: snowflake.ingress.guard_warehouse_export
What it does
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
external data sharing. It inspects the SQL string carried by the SQL-passthrough
tools (SYSTEM_EXECUTE_SQL, run_snowflake_query, write_query) and denies the
request at ingress, before the SQL ever reaches Snowflake, when the query
contains any of these constructs (matched case-insensitively):
COPY INTO @<stage>— bulk unload of a table to a named/internal stage.COPY INTO '<scheme>://…'— bulk unload directly to a cloud-storage URL outside Snowflake. Any quoted URL literal afterCOPY INTOis treated as an external unload target regardless of scheme (s3://,s3gov://GovCloud,gcs://,azure://,s3compat://, …); the scheme list is not enumerated.CREATE [OR REPLACE] STAGE— creates the staging object that unloads target.CREATE SHARE,ALTER SHARE … ADD ACCOUNTS, andGRANT … TO SHARE— the three steps of external data sharing (create the share, add consumer accounts, and place regulated objects into it). All are denied so no single step of an off-perimeter share can be built by the agent.PUT file://…andGET @<stage>— client-side file transfer to/from a stage.
Everything else — reads, SELECT, ordinary DML, and COPY INTO <table> FROM @<stage> (a load, not an unload) — passes through unchanged. This is a
distinct job from the destructive-mutation policy (guard-warehouse-sql):
that policy governs DROP/TRUNCATE/GRANT-class governance changes, whereas
this one governs data leaving the warehouse.
Why no group is exempt
Unlike the mutation policy — which exempts a data-platform-admins IdP group —
bulk export of regulated data off-perimeter is never an agent-appropriate
action, even for a data-platform admin. There is no allow if claims branch: a
human should run these operations out of band, from a session that is directly
attributable to them, not through an agent that can be prompt-injected. If your
organization needs a break-glass path, run it outside the gateway rather than
weakening this policy.
Compliance alignment
- SOC 2 CC6.7 — supports the restriction on transmission, movement, and removal of information by blocking bulk unload of warehouse data to external storage and shares; CC8.1 — treats stage/share creation (a governance change to how data can leave) as a change-managed operation the agent may not self-serve.
- PCI DSS 7.2.6 — supports restricting programmatic query access to stored
cardholder data by role, by denying the constructs that copy that data out;
3.4.2 — supports preventing the copy/relocation of PAN via remote access
by blocking
COPY INTOto external targets over the agent channel. - GDPR Art. 5(1)(c) — supports data minimisation by preventing wholesale export of personal data; Arts. 44/46 — supports the restriction on cross-border transfers by blocking agent-initiated unload to arbitrary cloud storage and external Snowflake accounts; Art. 5(1)(f)/32 — security of processing on the agent's warehouse-egress path.
- HIPAA §164.502(e) / §164.514(d) — supports the business-associate and
minimum-necessary safeguards on a PHI-capable warehouse: blocking bulk
unload (
COPY INTOan external stage or cloud-storage URL) and external data shares stops an agent from copying PHI-bearing tables wholesale to storage or Snowflake accounts outside the covered entity's controlled, BAA-governed perimeter.
Tool name matching
Snowflake has no stable canonical tool names — the managed MCP server lets
the admin name each tool freely (the SQL semantics live in the tool type,
which is not visible on the wire), and the SQL surface is a string inside one
argument. This policy matches the SQL-execution tools by suffix on
lower(input.resource.name):
*execute_sql(covers the managed server'sSYSTEM_EXECUTE_SQLtype when surfaced under that name)*run_snowflake_query(Snowflake-Labsrun_snowflake_query)*write_query(communitymcp-snowflake-serverwrite_query)
Because managed-server tool names are admin-chosen, add the exact SQL-tool
name your deployment configured to sql_tool_suffixes, and pair this policy
with a PF-28 default-deny-unknown-tools policy so a newly added or renamed SQL
tool cannot slip past the suffix list. Verify names with the dump-input debug
technique before relying on this in production.
Argument shape
For all three tools the SQL text is the entire policy surface. The landscape
note verifies the argument key is query. To be robust against alternate key
names on admin-configured tools, and to defeat "hide the SQL under a different
key" bypasses, the policy scans every top-level string-valued argument (not
just query) and matches the export patterns against their concatenation. Only
SQL-execution tools are inspected, so scanning all string args cannot affect
read tools.
Examples
Allowed
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "snowflake-mcp-run_snowflake_query", "type": "tool" },
"payload": {
"name": "snowflake-mcp-run_snowflake_query",
"args": { "query": "SELECT id, name FROM analytics.customers LIMIT 100" }
}
}
}
allow = true, no reason. (A COPY INTO customers FROM @load_stage load is
likewise allowed — only unloads to a stage/URL are blocked.)
Denied
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "snowflake-mcp-run_snowflake_query", "type": "tool" },
"payload": {
"name": "snowflake-mcp-run_snowflake_query",
"args": { "query": "COPY INTO @my_ext_stage FROM prod.pii.customers" }
}
}
}
allow = false, reason names the construct ("COPY INTO @ …").
Composition
guard-warehouse-sql— the sibling ingress policy that blocks destructive/governance SQL (DROP/TRUNCATE/DELETE/GRANT,CREATE USER/ROLE) with adata-platform-adminsexemption. Attach both: this one has no exemption by design.- A PF-28
default-deny-unknown-toolspolicy on the Snowflake server prefix, so admin-named SQL tools that are not on the suffix list are denied outright rather than passing uninspected. - An egress redaction policy on read tools (
read_query,run_snowflake_query, Cortex Search/Analyst): that only masks what the agent reads back; this policy prevents the write that exports the data. They are complementary, not substitutes.
Known limitations
- Regex over SQL text, not a parser. The patterns are conservative and
anchored per construct, but SQL is expressive: heavy comment injection
(
COPY/*x*/INTO), unusual quoting, or vendor syntax variants could evade a pattern. Treat this as a high-signal guardrail, not a complete anti-exfil control. Deeper coverage belongs in Snowflake-side network policies and storage-integration allowlists. - Nested/structured arguments not inspected. Only top-level string arguments
are scanned. SQL nested inside an object (e.g.
args.options.query) or inside an array (e.g. a batchargs.statements: ["COPY INTO @…"]) is not matched — the comprehension takes onlyis_string(v)top-level values. None of the three verified tools use those shapes (each takes a singlequery/statement string); add a rule (or a recursive walk) if your admin-configured tool nests SQL. Pair with a PF-28default-deny-unknown-toolspolicy so such a tool cannot be added silently. - Composite tools are opaque. A
CORTEX_AGENT_RUN-type tool executes multi-step plans server-side; the gateway sees one opaque call and cannot reach the SQL inside it. Deny agent/composite tools separately (see the landscape note's PF-06/PF-14 candidates). - Tool names are unverified for the managed server.
SYSTEM_EXECUTE_SQLis a tool type, not a wire name; the actual tool name is admin-chosen. The suffixes here match the two OSS servers' verified names plus a generic*execute_sql; confirm and extendsql_tool_suffixesfor your deployment. - No identity exemption. This is intentional (see "Why no group is exempt").
The policy does not read
input.subject.claimsat all.
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 snowflake.ingress.guard_warehouse_export
# Deny-by-default: only the explicit allow rules below permit the request.
default allow := false
# SQL-execution tools whose `query`/statement argument carries arbitrary SQL.
# Snowflake has no canonical tool names, so we match by suffix on the tool name.
# `execute_sql` covers the managed server's SYSTEM_EXECUTE_SQL type when surfaced
# under that name; the other two are the verified OSS-server names. Add the exact
# SQL-tool name your managed-server deployment configured.
sql_tool_suffixes := [
"execute_sql",
"run_snowflake_query",
"write_query",
]
is_sql_tool if {
name := lower(input.resource.name)
some suffix in sql_tool_suffixes
endswith(name, suffix)
}
# Bulk-export / external-staging constructs. Each pattern is case-insensitive
# (`(?i)`) and anchored to a specific SQL construct to limit false positives.
export_constructs := [
# Unload a table into a named or internal stage: COPY INTO @<stage> ...
# (COPY INTO <table> FROM @stage is a *load* and does not match — the @ must
# directly follow INTO).
{
"pattern": `(?i)\bcopy\s+into\s+@`,
"label": "COPY INTO @<stage> (bulk unload to a stage)",
},
# Unload directly to a cloud-storage URL outside Snowflake. Any quoted URL
# literal directly after COPY INTO is an external unload target — the scheme
# is NOT enumerated (covers s3://, s3gov:// GovCloud, gcs://, azure://,
# s3compat:// and any future scheme). A *load* names an unquoted table
# (COPY INTO <table> FROM …), so requiring the leading quote avoids the load.
{
"pattern": `(?i)\bcopy\s+into\s+'[a-z0-9][a-z0-9+.\-]*://`,
"label": "COPY INTO cloud-storage URL (external unload)",
},
# Create the staging object that COPY INTO unloads to.
{
"pattern": `(?i)\bcreate\s+(?:or\s+replace\s+)?(?:temp(?:orary)?\s+)?stage\b`,
"label": "CREATE STAGE",
},
# Create a data share (exposes data to other Snowflake accounts).
{
"pattern": `(?i)\bcreate\s+(?:or\s+replace\s+)?share\b`,
"label": "CREATE SHARE",
},
# Add external accounts to an existing share. [\s\S]* crosses newlines
# because ADD ACCOUNTS may sit on a later line of the statement.
{
"pattern": `(?i)\balter\s+share\b[\s\S]*\badd\s+accounts\b`,
"label": "ALTER SHARE ... ADD ACCOUNTS",
},
# Grant object access into an existing share — the step that actually places
# regulated tables into a share for external accounts to read (CREATE/ALTER
# SHARE alone expose nothing without it). `\bto\s+share\b` requires the SHARE
# keyword, so it does not match GRANT ... TO ROLE share_admin or a table whose
# name merely contains "share".
{
"pattern": `(?i)\bgrant\b[\s\S]*\bto\s+share\b`,
"label": "GRANT ... TO SHARE (expose objects to a data share)",
},
# Client-side upload of a local file to a stage.
{
"pattern": `(?i)\bput\s+'?file://`,
"label": "PUT (upload to stage)",
},
# Client-side download of stage contents to the local filesystem.
{
"pattern": `(?i)\bget\s+@`,
"label": "GET (download from stage)",
},
]
# Concatenate every top-level string argument so SQL cannot hide under an
# alternate key. Missing `args` yields "" (fail-safe: nothing to match).
sql_text := concat("\n", [v |
some _, v in object.get(input.payload, "args", {})
is_string(v)
])
# Which export constructs the query text triggers.
matched_constructs contains label if {
is_sql_tool
some entry in export_constructs
regex.match(entry.pattern, sql_text)
label := entry.label
}
# Allow anything that is not a SQL-execution tool.
allow if {
not is_sql_tool
}
# Allow SQL-execution tools only when no export construct matches.
allow if {
is_sql_tool
count(matched_constructs) == 0
}
reasons contains msg if {
is_sql_tool
some label in matched_constructs
msg := sprintf("This Snowflake SQL performs a bulk export or external-sharing operation (%s), which the agent is not permitted to run. Bulk export of warehouse data off Snowflake's perimeter must be run by a human out of band — no IdP group is exempt. Contact your data-platform team if this was a false positive.", [label])
}
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)
Used in these guides
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 Calendar Invites to External Attendees
Denies Google Calendar event-write tool calls — create event / create-event, update event / update-event, and the consolidated manage event — whenever any…
google-calendarguard-external-sendingresscalendarsoc2hipaagdpr-ccpa