Guard Vendor Banking and Tax-ID Changes
Blocks create vendor and update vendor calls whose arguments carry a vendor's payment coordinates — bank account number, routing / ACH branch details — or…
- Direction
- ingress
- Rego package
quickbooks.ingress.guard_vendor_banking- App
- quickbooks
- Bundle
- sox
- Published
- Minimum gateway
- 1.0.0b24
- Schema version
- 1.0.0
- Checksum
sha256:ffd9a0fd618f61d9ba6efbc0727f4f73cfd95d3ae4a36915ab474314b7053134
quickbooksvendor-bankinganti-becingresssox
What this policy does
Direction: ingress (tool_pre_invoke)
Default: deny on match, allow otherwise
Package: quickbooks.ingress.guard_vendor_banking
What it does
Blocks create_vendor and update_vendor calls whose arguments carry a
vendor's payment coordinates — bank account number, routing / ACH branch
details — or its tax identity — the EIN/SSN used for 1099 reporting. Any
vendor create/update that touches one of these fields is denied at ingress,
before it reaches the QuickBooks MCP server, so the change never lands in the
books of record.
Silently repointing a vendor's bank account is the core business-email- compromise (BEC) vector: an injected agent instruction that rewrites a vendor's ACH details quietly reroutes every future payment to that vendor to an attacker-controlled account. Because the mutation looks like an ordinary vendor edit, it is easy to miss in a review of agent activity — so the gateway refuses it outright and points the caller at the human, dual-approval path in QuickBooks.
Non-banking vendor edits — display name, print-on-check name, payment terms,
email, phone, billing address — pass through unchanged. Vendor
deletes/deactivations are out of scope here; they are covered by the
companion freeze-destructive-ops policy.
Compliance alignment
- SOX — Exchange Act Rule 13a-15(f)(3) (17 CFR §240.13a-15), safeguarding of assets. Preventing unauthorized change to a vendor's payment coordinates is a direct "prevent or timely detect unauthorized … disposition of assets" control on the agent path — a rerouted ACH account is asset disposition to an unauthorized party. The gateway logs every attempt with a deny reason.
- SOX — COSO 2013 Principle 10 (segregation of duties). Blocking the agent from setting vendor banking/tax details keeps the "who can change where money goes" step in a human, dual-approval lane rather than letting an agent both initiate and effect it.
- SOC 2 CC6.1 (logical access controls) / PI1.5 (integrity of stored records). Denying agent-initiated changes to a vendor's payment coordinates and tax identity is a logical-access boundary that prevents unauthorized modification of financial master data over the agent channel, supporting the integrity of the vendor records QuickBooks holds.
This policy addresses the PF-10 family (guard-vendor-banking) row of the
coverage matrix (SOX §2.5, Rule 13a-15(f)(3)).
Tool name matching
The policy matches the vendor create/update tools by suffix on the
separator-normalized tool name — input.resource.name lowercased with
underscores, hyphens, and spaces stripped:
*createvendor(matchescreate_vendor,createVendor,create-vendor)*updatevendor(matchesupdate_vendor,updateVendor,update-vendor)
Normalizing the tool name means a server that uses camelCase or hyphenated
tool names cannot silently bypass the policy (a plain endswith on
create_vendor would miss createVendor and no-op the whole policy). Matching
the createvendor / updatevendor verb+entity suffix still targets the
_vendor entity while create_vendor_credit / update_vendor_credit (a
different entity, normalizing to ...vendorcredit, ending in credit) is
naturally excluded, and vendor reads (get_vendor, search_vendors) and
deletes (delete_vendor) fall through to allow — deletes are handled by
freeze-destructive-ops, not here.
The DTwo gateway prefixes tool names with the configured MCP server name (e.g.
qbo-mcp-create_vendor), and that prefix is not standardized — suffix matching
keeps the policy portable. Verify the exact names your gateway sends with a
live tools/list (or the dump-input debug technique) before relying on this in
production.
Argument shape
The policy inspects field names in input.payload.args, recursively
(including nested objects), and normalizes each key (lowercase, underscores /
hyphens / spaces removed) so it matches both server conventions:
- Intuit official server — snake_case wrapper keys, e.g.
bank_account_number,routing_number,tax_identifier,vendor_payment_bank_detail. - LibreChat community server — raw-QBO PascalCase keys, e.g.
BankAccountNumber,BankBranchIdentifier,TaxIdentifier,VendorPaymentBankDetail.
Normalization collapses both to the same token (bankaccountnumber,
taxidentifier, …), so the single sensitive_fields allowlist covers both
shapes. As a defense-in-depth second branch, the policy also denies when any
string value in the payload is shaped like a US tax identifier (SSN
123-45-6789 or EIN 12-3456789) — this catches a tax ID smuggled under a
benign key.
The exact QBO Vendor bank/tax field keys are not verified in the app landscape note. Treat
sensitive_fieldsas a documented candidate allowlist to confirm against a livetools/listfor your server, and tune it to the keys your deployment actually emits (see Known limitations).
Examples
Allowed — non-banking vendor edit
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "qbo-mcp-update_vendor", "type": "tool" },
"payload": {
"name": "qbo-mcp-update_vendor",
"args": { "id": "56", "display_name": "Acme Supplies", "terms_ref": "NET30" }
}
}
}
allow = true, no reason.
Denied — bank account on a vendor create
{
"input": {
"action": "tool_pre_invoke",
"resource": { "name": "qbo-mcp-create_vendor", "type": "tool" },
"payload": {
"name": "qbo-mcp-create_vendor",
"args": {
"display_name": "New Vendor LLC",
"bank_account_number": "000123456789",
"routing_number": "021000021"
}
}
}
}
allow = false, reason names the BEC risk and the dual-approval path.
Composition
This policy is single-purpose. Useful companions on the same QuickBooks gateway:
freeze-destructive-ops— deniesdelete_vendor(deactivation) and other destructive verbs.gate-money-movement(PF-09) — caps/deniescreate_payment/create_bill_paymentso a mis-set vendor cannot be paid at scale.role-gate-writes(PF-12) — restricts all vendor writes to a finance IdP group as the least-privilege baseline.- An egress PII/DLP policy that redacts SSN/EIN/bank-account values from
get_vendor/search_vendorsresponses.
Known limitations
- Field-name allowlist is unverified. The exact QuickBooks Vendor
bank/tax field keys are not confirmed in the landscape note.
sensitive_fieldsis a candidate list — confirm it against a livetools/listand add any keys your server uses (some servers may nest bank details under a container object with a name not in the list). Missing a key means that field is not blocked. - Shapeless values in free-text fields. A bank account number pasted into a
benign free-text field (e.g.
print_on_check_name,notes, a QBOCustomFieldStringValue, or a stringified-JSON blob whose keys are not real object keys) has no fixed shape and no sensitive key name, so it is not caught — only the tax-ID value branch (SSN/EIN shapes) inspects values, and the field-name branch inspects only real object keys, not the contents of a string. Pair with an egress DLP policy for the read path if this residual matters. - Field-name matching is exact on the normalized token, not substring. The
banking-synonym list was broadened after red-team review (adds
bankaccountno,accountno,aba/abanumber/abaroutingnumber,wireroutingnumber,iban,swift/swiftcode,bic,sortcode), but a key must normalize to a token that is exactly in the set — a novel key such asvendor_bank_acct_number(normalizes tovendorbankacctnumber) will not match. Confirm the keys your server actually emits and extend the list. - Tax-ID value branch can over-block. The dash-delimited value regex will
also fire on a benign value that happens to share the SSN (
\d{3}-\d{2}-\d{4}) or EIN (\d{2}-\d{7}) grouping — e.g. a foreign registration number or an oddly-formatted reference. Because this is a deny policy the over-block is fail-safe (the caller is pointed at finance), but tune the pattern or the scope if legitimate dash-delimited values in your data collide. - Tax-ID value regex is US-shaped and dash-delimited only. The value branch
matches the dash-delimited US SSN (
123-45-6789) and EIN (12-3456789) formats only. A tax ID written without separators (123456789) or with spaces (123 45 6789) under a benign free-text key is not caught by the value branch — matching bare 9-digit runs would over-block every order number, phone, and quantity, so the pattern is deliberately conservative. Non-US tax identifiers are likewise caught only by field name. This is defense-in-depth behind the field-name allowlist, which remains the primary control; pair with an egress DLP policy if the read path matters. - Parameterized / mega-tool servers are not covered. This policy matches on
the
create_vendor/update_vendortool-name suffix, which fits the Intuit official server and the LibreChat community server (verb_entitynaming). It does not cover servers that expose a single parameterized tool and carry the verb+entity in an argument — e.g. the archivedhvkshetry/quickbooks-mcppartytool called asparty(operation="update", party_type="vendor", …). Such a call has a tool name (party) that matches neither suffix, so banking and tax fields in its arguments pass through unblocked. If your deployment uses a parameterized server, add a companion policy that inspects theoperation/party_type(or equivalent) arguments; a suffix-matching policy alone cannot see the verb. - No identity-based exemptions. All callers are subject to the same check.
To allow a break-glass finance controller to set banking details via the
agent, add an
allow ifbranch gated oninput.subject.claims.groups(group names are placeholders — replace with your IdP's group name at import time). - Reads and deletes are out of scope. Vendor reads pass through; vendor
deletes/deactivations are governed by
freeze-destructive-ops.
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 quickbooks.ingress.guard_vendor_banking
# Deny-by-default: only the explicit allow rules below permit the request.
default allow := false
# Normalized (lowercase, no separators) vendor field-name tokens that carry
# banking or tax-identity data. CANDIDATE LIST — the exact QBO Vendor keys are
# not verified; confirm against a live tools/list and tune per deployment.
# Normalization collapses snake_case (official server) and PascalCase
# (LibreChat raw-QBO) to the same token, so one list covers both conventions.
sensitive_fields := {
# --- Banking / ACH payment coordinates ---
"bankaccountnumber",
"accountnumber",
"bankaccount",
"routingnumber",
"bankroutingnumber",
"achroutingnumber",
"bankbranchidentifier", # QBO routing/branch identifier
"vendorpaymentbankdetail", # QBO nested bank-detail container
"bankaccountdetail",
"achenabled",
# Common banking-identifier synonyms / abbreviations and intl. equivalents
# (added after red-team review — all are bank routing/account identifiers,
# not plausible benign vendor field names).
"bankaccountno",
"accountno",
"aba",
"abanumber",
"abaroutingnumber",
"wireroutingnumber",
"iban",
"swift",
"swiftcode",
"bic",
"sortcode",
# --- Tax identity (EIN/SSN for 1099) ---
"taxidentifier", # QBO TaxIdentifier -> tax_identifier
"taxid",
"taxidentificationnumber",
"taxregistrationnumber",
"ein",
"ssn",
"tin",
}
# US tax-identifier value shapes: SSN 123-45-6789 or EIN 12-3456789.
# Anchored with word boundaries to stay conservative (won't match a longer
# digit run). Catches a tax ID smuggled under a non-sensitive key name.
tax_id_value_pattern := `\b(\d{3}-\d{2}-\d{4}|\d{2}-\d{7})\b`
# Tool arguments, safely defaulted so a missing `args` yields an empty object
# rather than a rule-body failure.
args := object.get(input.payload, "args", {})
# Normalize a field name: lowercase and strip underscores, hyphens, spaces so
# `bank_account_number` and `BankAccountNumber` compare equal.
normalize(key) := lower(regex.replace(key, `[_\-\s]`, ""))
# Vendor create/update tools. We match on the SEPARATOR-NORMALIZED tool name
# (lowercase + underscores/hyphens/spaces stripped) so `create_vendor`,
# `createVendor`, and `create-vendor` all match — otherwise a server using
# camelCase or hyphenated tool names would silently bypass the whole policy.
# Matching the `createvendor` / `updatevendor` suffix targets the `_vendor`
# entity and still naturally excludes `create_vendor_credit` /
# `update_vendor_credit` (normalizes to `...vendorcredit`, ends in `credit`)
# and `delete_vendor` / `get_vendor` / `search_vendors`.
is_vendor_write if {
endswith(normalize(input.resource.name), "createvendor")
}
is_vendor_write if {
endswith(normalize(input.resource.name), "updatevendor")
}
# True if any argument key (at any depth) is a banking/tax-identity field.
banking_field_present if {
walk(args, [path, _])
some key in path
is_string(key)
sensitive_fields[normalize(key)]
}
# True if any string value (at any depth) is shaped like a US tax identifier.
tax_id_value_present if {
walk(args, [_, value])
is_string(value)
regex.match(tax_id_value_pattern, value)
}
# Allow anything that isn't a vendor create/update call (reads, deletes,
# vendor-credit tools, and every non-vendor tool).
allow if {
not is_vendor_write
}
# Allow vendor create/update only when no banking/tax field or tax-ID-shaped
# value is present.
allow if {
is_vendor_write
not banking_field_present
not tax_id_value_present
}
reasons contains "Creating or updating a vendor with bank-account, routing/ACH, or tax-identity (EIN/SSN) fields is blocked at the gateway. Silently repointing a vendor's payment coordinates is the primary business-email-compromise (BEC) vector: a rerouted bank account diverts every future ACH payment. Change vendor banking or tax-ID details directly in QuickBooks under dual approval, or ask your finance/AP administrator to make the change or grant an exception." if {
is_vendor_write
banking_field_present
}
reasons contains "This vendor create/update carries a value shaped like a US tax identifier (SSN or EIN). Tax IDs for 1099 vendors must be set in QuickBooks under finance review, not through the agent. Contact your finance/AP administrator if this change is legitimate." if {
is_vendor_write
tax_id_value_present
}
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
Block Destructive and Mutating Snowflake SQL
Inspects the SQL text that Snowflake MCP tools carry in their query argument and denies any statement in a mutating or destructive class — DROP, TRUNCATE,…
snowflakeguard-warehouse-sqlingresssqlreadonlysoc2pci-dsssox
Block Destructive SQL in BigQuery Queries
Inspects the raw GoogleSQL string carried by BigQuery write-capable query tools and denies any statement in a state-changing class — DML…
Cap QuickBooks Bulk Search Exports
Clamps the bulk-read levers on every QuickBooks Online search tool so an agent cannot pull the entire general ledger — or a full customer, vendor, or…
quickbookscap-bulk-exportbulk-exportdlpingresssoc2pci-dssgdpr-ccpa
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…
Freeze Destructive Microsoft 365 Operations
Denies every Microsoft 365 tool call whose verb segment is delete- or cancel- unless the caller's IdP token carries the placeholder group m365-admin.
Freeze Destructive QuickBooks Operations
Denies every destructive QuickBooks Online (QBO) tool call on the agent channel before it reaches the MCP server.