Relayer: whitelist_receivers does not cover the sign endpoints
Written 7 October 2026
Applies to OpenZeppelin Relayer 1.8.0 on EVM networks. Upstream issue: openzeppelin-relayer #912. Line numbers below are from the 1.8.0 source.
whitelist_receivers bounds where a relayer sends: the relayer refuses a transaction whose to is not on the list, the zero address or its own address. The relayer also has two signing endpoints, POST / (EIP-191) and POST / (EIP-712), and neither consults any policy.
Where the policy is applied
validate_ in src/ (lines 100 to 118) compares request.to with whitelist_receivers, after adding the zero address and the relayer's own address to the allowed set. It is called once, from the validation of EvmTransactionRequest (line 41). No other code path references it. The docs match: docs/evm.mdx line 103 describes the policy as a "list of authorized contract addresses for transactions".
The sign endpoints
In src/, sign_data (line 721) and sign_typed_data (line 761) load the relayer, call validate_active_state, and hand the request to the network relayer. In src/, sign_data (lines 512 to 516) and sign_typed_data (lines 535 to 542) call self.signer with nothing in between.
SignTypedDataRequest (src/ line 659) has two fields, domain_separator and hash_struct_message, both hex strings. construct_ in src/ (line 187) checks that each decodes to 32 bytes, builds \x19\x01 || domainSeparator || hashStruct, hashes it and signs the hash. The relayer never sees the typed data. It could not apply a policy to the payee, the amount, the token or the chain even if one existed, because none of them reach it. Every EVM signer except CDP does this: AWS KMS, Google Cloud KMS, Turnkey, Azure Key Vault and the local signer directly, and Vault through the local signer. CDP refuses sign-typed-data because it needs the full typed data (cdp_signer.rs lines 180 to 191).
Consequences
Anyone holding the relayer API key can obtain a signature over any EIP-712 struct hash, or any EIP-191 message, from your key. There is one API key for the whole instance; per-key scopes are the subject of upstream issue #479, open since 2025-09-26.
If the relayer address holds a token that supports EIP-3009 (USDC does) or any token it has approved to Permit2, that signature is a transferWithAuthorization or a Permit2 permit to any recipient for any amount, which anyone can submit onchain. whitelist_receivers never sees it. This is the signing path x402 payments use, so any relayer that signs x402 payments has this exposure.
As long as the address has no EIP-7702 delegation, its own native balance cannot be moved this way. /sign applies the EIP-191 prefix (\x19Ethereum Signed Message:\n) to the message's UTF-8 bytes in every signer (so "0xdeadbeef" is signed as ten characters of text, not four bytes), and /sign-typed-data the \x19\x01 prefix, so neither can produce a transaction signature or an EIP-7702 authorization. A delegated address runs its delegate's code, and smart-account code usually accepts exactly these signatures. Anything else that accepts the address's signature is in reach too, on every EVM chain, since the address is the same everywhere and the endpoint never sees the chain: EIP-2612 permits on tokens it holds, a Safe where it is an owner (Safe transactions are EIP-712), an Allowance Module delegation, an ERC-2771 forwarder trusted by a contract where the address holds a role, an offchain order or vote, a Sign-In with Ethereum session.
The same API key also authorizes PATCH /, which applies a JSON merge patch to the relayer (apply_json_patch in src/). Sending "policies": {"whitelist_ removes the list. So an agent or service that holds the key can loosen its own policy before it uses it.
Mitigations
- Keep only gas on the relayer address, approve nothing from it, and give it no authority that is exercised by signature, on any chain: not a Safe or smart-account owner or delegate, not a signer that some contract or forwarder trusts, and never an EIP-7702 delegation.
- If an agent or a service needs typed-data signatures from the relayer (x402, permits, meta-transactions), put a service in front of
sign-typed-datathat takes the full typed data, checksdomain.chainIdandverifyingContractagainst an allowlist, checks the payee and the amount, boundsvalidBefore, keeps a running total per period, and only then computes the two hashes and calls the relayer. The relayer itself has no hook for this. - Treat the API key as the key. It is not a scoped credential, and it can change the policies.
- On Turnkey, the relayer signs messages and typed data as raw payloads with
HASH_FUNCTION_NO_OPand sends EVM transactions asACTIVITY_. A Turnkey policy that denies the raw payloads closes both sign endpoints at the provider, where nobody holding the relayer API key can change it, and transactions still go through. If that policy pins recipients and functions, also allow zero-value, empty-data transactions from the relayer address to itself: they are the no-op the relayer uses to cancel and to fill nonce gaps.TYPE_ SIGN_ TRANSACTION_ V2 PATCHthe relayer with{"paused": true}to refuse new sends, cancels and both sign endpoints (validate_active_stateruns in the API controllers). Transactions already queued or pending are still signed, sent and bumped by the background workers, and with the default in-memory storage the pause is lost on restart.
The EVM roadmap in the repo does not list anything here. The Stellar roadmap does list "transaction amount limits, destination whitelisting, time bound and limit operations". Upstream: openzeppelin-relayer #912.