dtwo Policy Store

Deny Stripe API-Write Escape Hatch

Denies the stripe api write meta-tool — the single raw passthrough on the official Stripe MCP server that can execute any Stripe POST, PATCH, PUT, or DELETE…

Direction
ingress
Rego package
stripe.ingress.deny_escape_hatches_api_write
App
stripe
Bundles
soxsoc2
Published
Minimum gateway
1.0.0b24
Schema version
1.0.0
Checksum
sha256:d3c004edbb6c5f65a9f8185b2372061a12e018d9d1340d79663eb5539eecfa37

stripedeny-escape-hatchesingresssoxsoc2

What this policy does

Direction: ingress (tool_pre_invoke) Default: deny Package: stripe.ingress.deny_escape_hatches_api_write

What it does

Denies the stripe_api_write meta-tool — the single raw passthrough on the official Stripe MCP server that can execute any Stripe POST, PATCH, PUT, or DELETE method (payouts, transfers, account mutations, refunds, subscription changes) and would otherwise bypass every named-tool policy — in two layers:

  1. Role gate: callers whose IdP groups claim does not include finance or billing-admin cannot use the passthrough at all.
  2. Endpoint hard stop: even for those groups, the call is denied when the serialized argument object contains a money-movement or account token — payouts, transfers, topups, financial_connections, Connect accounts, or any connected-account id (acct_…). The account tokens are bare (not /v1/accounts) so they catch account mutation and Connect money routing regardless of how the passthrough names the field (a path string, a resource key, or a transfer_data.destination / on_behalf_of id).

All other tools pass through unchanged. The check runs at ingress, so a blocked call never reaches Stripe and no side effect occurs.

Compliance alignment

  • PCI DSS 7.2.1 / 7.2.2 — supports the least-privilege access model: the raw write passthrough is a privileged channel into the payment account, and this policy restricts it to defined roles with defined endpoint limits. 7.2.5 — supports least privilege for the agent's application account by narrowing what its Stripe grant can reach over MCP.
  • SOC 2 CC6.1 — supports logical access security over protected assets; CC6.3 — supports role-based access and least privilege on the one tool that collapses Stripe's entire write surface into a single name.
  • SOX ITGC (access to programs & data) — supports least-privilege access to a financial system's write path; Rule 13a-15(f)(3) — supports safeguarding of assets by blocking payout, transfer, top-up, and account-mutation endpoints outright on the agent channel.

Why ingress and not egress

stripe_api_write executes irreversible, externally visible writes — a payout that has left the balance cannot be recalled by redacting the response. Ingress denial is the only placement that actually prevents the action.

Tool name matching

Matches by suffix on lower(input.resource.name):

  • *stripe_api_write — the official server's passthrough (verified from docs.stripe.com/mcp)
  • *api_write — broader stem for renamed deployments that keep the suffix

The DTwo gateway prefixes tool names with the configured MCP server name (e.g. stripe-mcp-stripe_api_write), and that prefix is not standardized — suffix matching keeps the policy portable. Verify the exact name your gateway sends with the dump-input debug technique before relying on this in production.

Argument shape

The exact argument field names of stripe_api_write (e.g. path vs method vs params) are not published and are unverified — the landscape research could not capture a live schema without an authenticated tools/list. The policy therefore does not index any specific key: it serializes the whole of input.payload.args with json.marshal and matches the endpoint tokens case-insensitively on lower(...) as substrings. This makes the endpoint hard stop hold regardless of the real key shape, including tokens nested arbitrarily deep in the argument object.

Examples

Allowed — finance caller, non-money endpoint

{
  "input": {
    "action": "tool_pre_invoke",
    "resource": { "name": "stripe-mcp-stripe_api_write", "type": "tool" },
    "subject": { "sub": "auth0|cfo", "claims": { "groups": ["finance"] } },
    "payload": {
      "name": "stripe-mcp-stripe_api_write",
      "args": { "path": "/v1/customers", "method": "POST", "params": { "name": "Acme" } }
    }
  }
}

allow = true, no reason.

Denied — caller outside finance/billing-admin

{
  "input": {
    "action": "tool_pre_invoke",
    "resource": { "name": "stripe-mcp-stripe_api_write", "type": "tool" },
    "subject": { "sub": "auth0|dev", "claims": { "groups": ["engineering"] } },
    "payload": {
      "name": "stripe-mcp-stripe_api_write",
      "args": { "path": "/v1/customers", "method": "POST" }
    }
  }
}

allow = false, reason directs the caller to the dedicated named tools.

Denied — money-movement endpoint, even for finance

{
  "input": {
    "action": "tool_pre_invoke",
    "resource": { "name": "stripe-mcp-stripe_api_write", "type": "tool" },
    "subject": { "sub": "auth0|cfo", "claims": { "groups": ["finance"] } },
    "payload": {
      "name": "stripe-mcp-stripe_api_write",
      "args": { "path": "/v1/payouts", "method": "POST", "params": { "amount": 500000 } }
    }
  }
}

allow = false for everyone — money movement goes through the dedicated named tools or the Stripe dashboard.

Composition

This policy closes the passthrough so it cannot reach a surface the named- tool policies restrict. Pair it with its Stripe companions from the same family set:

  • the refund-cap policy (PF-09), which bounds *create_refund amounts — without this policy, stripe_api_write could issue an uncapped refund directly against /v1/refunds;
  • the role-gate / read-only policy (PF-12), which restricts the named write tools (*create_*, *update_*, *cancel_subscription, …) to the same finance groups.

Layer all of them with Stripe Restricted API Key (RAK) scoping — DTwo policy and key scoping are complementary control planes, not either/or.

Known limitations

  • Group names are placeholders — replace finance and billing-admin with your IdP's group names at import time. Missing or malformed groups claims fail closed (the caller is treated as unprivileged).
  • Unverified argument schema. The endpoint tokens are matched as substrings of the serialized argument object because the passthrough's field names are unverified. Capture a live schema from your gateway and tighten the match to the real endpoint key if you need fewer false positives.
  • Substring false positives. A privileged caller writing a benign value that merely mentions a token (e.g. a description string containing "payouts", or any argument carrying a connected-account acct_… id) is denied. On Connect platforms the accounts / acct_ tokens will deny a broad range of connected-account operations — this is the deliberate fail-closed trade-off for the account hard stop; the deny reason carries an escalation hint.
  • Parameter-level money movement not fully covered. The hard stop keys on endpoint/account tokens, not on every money-moving parameter. A Connect charge that routes money via transfer_data.destination or on_behalf_of is caught because those values carry an acct_… id, but a direct-charge application_fee_amount with no account reference carries none of the tokens and passes the endpoint check (the group gate still applies). Money movement that must name a destination account is covered; fee-only parameters on non-money endpoints are a residual — pair with RAK scoping.
  • Missing args pass the endpoint check. A privileged caller invoking the passthrough with no arguments at all serializes to {} and passes the endpoint hard stop (the group gate still applies). Such a call carries no endpoint and fails at the Stripe API anyway.
  • Obfuscation residual. Endpoint strings encoded (base64, URL-escaped, split across fields) would not match the tokens — though such values would also not be valid Stripe method identifiers. RAK scoping is the backstop control plane.
  • Scope. Legacy per-resource write tools (create_refund, update_subscription, …) are governed by the companion role-gate and refund-cap policies, not this one. Stripe Treasury preview tool names are unpublished/unverified and are not covered. Composio's STRIPE_* action slugs do not share the api_write suffix and are out of scope.

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 stripe.ingress.deny_escape_hatches_api_write

# Deny-by-default: only the explicit allow rules below permit the request.
default allow := false

# IdP groups permitted to touch the raw API-write passthrough at all.
# Placeholders — replace with your IdP's group names at import time.
allowed_groups := {"finance", "billing-admin"}

# Endpoint tokens that indicate money movement or account mutation. Matched
# case-insensitively as substrings of the serialized argument object because
# the passthrough's argument field names are unverified (see Known
# limitations in the description).
blocked_endpoint_tokens := [
	# /v1/payouts — money out of the Stripe balance to an external account
	"payouts",
	# /v1/transfers — Connect money movement between accounts
	"transfers",
	# /v1/topups — funding the Stripe balance from a bank account
	"topups",
	# accounts — Connect account creation/mutation/deletion. Bare token (NOT
	# "/v1/accounts") so it matches whatever shape the passthrough uses to
	# express the endpoint (a "/v1/accounts" path, a "resource": "accounts"
	# key, external_accounts/bank_accounts sub-resources). "/v1/accounts" alone
	# would only catch the full-path form and miss the others.
	"accounts",
	# acct_ — any reference to a connected-account id (destination charges,
	# transfer_data.destination, on_behalf_of, direct account updates) is
	# Connect money-movement / account-mutation surface expressed by id rather
	# than by endpoint path.
	"acct_",
	# /v1/financial_connections — linked bank-account sessions and data
	"financial_connections",
]

# Tool name, lowercased; empty string when the resource block is absent.
tool_name := lower(object.get(object.get(input, "resource", {}), "name", ""))

# The official Stripe server's raw write passthrough. Suffix match keeps the
# policy portable across gateway server-name prefixes. Verify the exact name
# your gateway sends with the dump-input debug technique.
is_api_write_tool if {
	endswith(tool_name, "stripe_api_write")
}

# Broader stem for renamed deployments that keep the api_write suffix.
is_api_write_tool if {
	endswith(tool_name, "api_write")
}

# Caller belongs to a group allowed to use the passthrough. Missing or
# malformed claims fail closed: no groups -> not privileged.
caller_is_privileged if {
	claims := object.get(object.get(input, "subject", {}), "claims", {})
	groups := object.get(claims, "groups", [])
	some g in groups
	lower(g) in allowed_groups
}

# Serialize the whole argument object so the endpoint check holds regardless
# of the passthrough's (unverified) argument key shape.
serialized_args := lower(json.marshal(object.get(object.get(input, "payload", {}), "args", {})))

args_reference_blocked_endpoint if {
	some token in blocked_endpoint_tokens
	contains(serialized_args, token)
}

# Any tool other than the API-write passthrough is out of this policy's scope.
allow if {
	not is_api_write_tool
}

# The passthrough is allowed only for privileged callers, and never toward
# money-movement or account endpoints.
allow if {
	is_api_write_tool
	caller_is_privileged
	not args_reference_blocked_endpoint
}

reasons contains "The Stripe API-write passthrough can execute any Stripe write and is restricted to the finance and billing-admin groups. Use the dedicated named Stripe tools for routine changes, or ask your Stripe administrator for access. Contact your InfoSec team if this was a false positive." if {
	is_api_write_tool
	not caller_is_privileged
}

reasons contains "This Stripe API-write call references a money-movement or account endpoint (payouts, transfers, topups, /v1/accounts, financial_connections), which is blocked for every caller on the agent channel. Use the dedicated named Stripe tools or the Stripe dashboard for money movement. Contact your InfoSec team if this was a false positive." if {
	is_api_write_tool
	args_reference_blocked_endpoint
}

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