- Location
- lib/intent-builder.js:236
- Finding
- Untrusted Uniswap API Calldata Is Forwarded for Signing Without Semantic Validation<![CDATA[
## Vulnerability Details
**File Location**: `lib/intent-builder.js:236-273` and `lib/intent-builder.js:339-349`
**Vulnerability Type**: Insufficient validation of remotely supplied transaction calldata
**Risk Level**: High
### Vulnerable Code
```js
const res = await fetch('https://api.uniswap.org/v2/quote', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
signal: controller.signal,
});
if (!res.ok) return null;
const data = await res.json();
// Validate response shape
if (!data.quote || !data.methodParameters) return null;
const { methodParameters, quote } = data;
const target = methodParameters.to;
const calldata = methodParameters.calldata;
const value = methodParameters.value;
// Safety: target must be the expected Universal Router
if (target.toLowerCase() !== UNISWAP.UNIVERSAL_ROUTER.toLowerCase()) return null;
// Safety: chainId in response must match request
if (data.chainId !== undefined && data.chainId !== chainId) return null;
// Safety: calldata must be present and non-empty hex
if (!calldata || calldata === '0x' || calldata.length < 10) return null;
// Safety: value must be numeric string
if (value === undefined || value === null) return null;
// Safety: value must not exceed requested amountIn (prevents overspend)
if (BigInt(value) > amountInWei) return null;
const amountOut = BigInt(quote.amount ?? 0);
if (amountOut === 0n) return null;
return { target, calldata, value: String(value), amountOut };
```
```js
// Primary: try Routing API
const apiResult = await tryRoutingAPI(chainId, weth, tokenOutAddress, amountInWei, maxSlippageBps);
if (apiResult) {
return {
target: apiResult.target,
calldata: apiResult.calldata,
value: apiResult.value,
chainHint: chainId.toString(),
quotedAmountOut: apiResult.amountOut,
source: 'routing-api',
};
}
```
The returned intent is subsequently submitted to the local signing daemon in `scripts/swap.js:48-59`:
```js
...[truncated 3544 chars]
- Remediation
- <![CDATA[
## Remediation Suggestions
1. Decode `methodParameters.calldata` before accepting the API response.
2. Require the expected Universal Router `execute` selector and reject all other functions.
3. Maintain a strict allowlist of router commands needed for the declared swap.
4. Verify that the decoded input token, output token, exact input amount, recipient, path, chain, deadline, and minimum output match locally computed expectations.
5. Reject additional commands or inputs that are not required for the requested swap.
6. Derive `amountOutMin` locally from a validated quote and the configured maximum slippage.
7. Prefer constructing all transaction calldata locally. Treat the remote endpoint only as a source of quote and route data.
8. Independently verify quotes using on-chain state before signing when practical.
9. Require the daemon to repeat the complete semantic validation rather than trusting the Skill.
10. Bind any approval prompt to a normalized intent digest and show the exact recipient, tokens, input, guaranteed minimum output, and router commands to the user.
11. Add negative tests containing valid router addresses but malicious recipients, extra commands, altered paths, zero minimum output, and mismatched amounts.
]]>