--- name: Deny Stripe API-Write Escape Hatch tags: - stripe - deny-escape-hatches - ingress - sox - soc2 publishedAt: 2026-07-12 description: | # stripe / deny-escape-hatches-api-write **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 ```jsonc { "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 ```jsonc { "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 ```jsonc { "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. direction: ingress apps: - stripe industries: [] bundles: - sox - soc2 experimental: true schemaVersion: 1.0.0 minimumGatewayVersion: 1.0.0b24 --- ```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) } ```