- Location
- src/payments/x402.ts:124
- Finding
- Facilitator-Controlled USDC Recipient and Amount Are Signed Without Validation<![CDATA[
## Vulnerability Details
**File Location**: `src/payments/x402.ts:91-97` and `src/payments/x402.ts:124-174`
**Vulnerability Type**: Insufficient validation of externally supplied transaction authorization
**Risk Level**: Critical
### Vulnerable Code
```ts
const requirements = (await requirementsRes.json()) as {
payTo: `0x${string}`;
amount: string;
token: `0x${string}`;
nonce: string;
deadline: string;
chainId: number;
};
```
```ts
const typedData = {
domain: {
name: "USDC",
version: "2",
chainId: requirements.chainId,
verifyingContract: requirements.token,
},
types: {
TransferWithAuthorization: [
{ name: "from", type: "address" },
{ name: "to", type: "address" },
{ name: "value", type: "uint256" },
{ name: "validAfter", type: "uint256" },
{ name: "validBefore", type: "uint256" },
{ name: "nonce", type: "bytes32" },
],
},
primaryType: "TransferWithAuthorization" as const,
message: {
from: wallet.address,
to: requirements.payTo,
value: BigInt(requirements.amount),
validAfter: 0n,
validBefore: BigInt(requirements.deadline),
nonce: requirements.nonce,
},
};
const signature = await wallet.signTypedData(typedData);
const paymentRes = await fetch(`${config.X402_FACILITATOR_URL}/pay`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
...typedData.message,
value: typedData.message.value.toString(),
validBefore: typedData.message.validBefore.toString(),
signature,
network: usdcNetwork,
}),
signal: AbortSignal.timeout(20_000),
});
```
### Technical Analysis
The facilitator supplies `payTo` and `amount`, and both fields are incorporated directly into an EIP-712 `TransferWithAuthorization` payload. The implementation verifies the chain ID, token address, and deadline, but it does not verify:
- `requirements.payTo === req.agentId`
- `BigInt(requirements.amount) === req.amount`
As
...[truncated 1524 chars]
- Remediation
- <![CDATA[
## Remediation Suggestions
- Validate the entire facilitator response with a strict schema before constructing typed data.
- Require exact equality:
```ts
if (requirements.payTo.toLowerCase() !== req.agentId.toLowerCase()) {
throw new PaymentError("Facilitator returned an unexpected payment recipient");
}
const authorizedAmount = BigInt(requirements.amount);
if (authorizedAmount !== req.amount) {
throw new PaymentError("Facilitator returned an unexpected payment amount");
}
```
- Validate that `payTo` is a nonzero EVM address.
- Validate `amount` as a positive integer within configured payment limits.
- Validate the nonce as exactly 32 bytes.
- Bind the requirements response to the original request using a signed or authenticated request identifier.
- Return the validated, actually authorized amount instead of `req.amount`.
- Consider requiring a trusted facilitator allowlist rather than accepting an arbitrary configured URL.
]]>