Security[SEC-I02 (H09)]: route intermediate token addresses not validated before swap submission [SEC-I02] #450

Closed
opened 2026-06-30 15:59:46 +00:00 by totdking · 17 comments
totdking commented 2026-06-30 15:59:46 +00:00 (Migrated from gitlab.com)
No description provided.
totdking commented 2026-06-30 16:08:54 +00:00 (Migrated from gitlab.com)

Summary

When the frontend displays a multihop route to the user (e.g. LUNC -> tokenX -> USTC), the intermediate token addresses come from the indexer response. The actual swap is built from the same indexer response's router_operations field. The frontend does not cross-validate that the intermediate token addresses displayed to the user match the token addresses in the submitted router_operations. A malicious indexer can display one route and submit a different token path, routing through unintended intermediate tokens within the user's slippage tolerance.

This is related to Issue (#449) but affects the intermediate path rather than the terminal pair contract. Together they represent the full trust-the-indexer attack surface for route integrity.


What Was Checked

  • frontend-dapp/src/utils/swapRouteDisplay.ts lines 65-110: computeSwapRouteDisplay() derives the displayed route from indexer operations.
  • frontend-dapp/src/utils/swapRouteDisplay.ts lines 115-131: deriveSwapSubmitRouteSource() uses indexer operations for submission without re-deriving expected intermediate tokens from router_operations to compare against the displayed path.
  • frontend-dapp/src/pages/SwapPage.tsx line ~531: only token_in and token_out are validated against the user's selected tokens. Intermediate token addresses in router_operations are accepted as-is from the indexer response.
  • frontend-dapp/src/services/terraclassic/swapRoutePreflight.ts: enrichment function adds min_return per hop but does not validate intermediate token addresses.

Expected (per checklist)

Before submitting a multihop swap, the frontend re-derives expected intermediate token addresses from the selected token pair and the number of hops, then confirms each hop's offer_asset and ask_asset in router_operations match the expected path. Any mismatch causes submission to be blocked with a clear error. Maps to SEC-E07 (route display aligns with actual submit operations).


Actual

No intermediate token address validation exists. The displayed path and submitted operations are both derived from the indexer response with no cross-check.


Suggested Fix

After receiving and accepting the indexer route response, extract the actual intermediate token sequence from router_operations (each hop's ask_asset_info becomes the next hop's offer_asset_info). Compare this derived sequence against the displayed intermediate_tokens array. If they do not match, reject the route and log a warning. This check runs client-side before enrichment and submission.


Verification Checklist

  • Frontend derives intermediate token sequence from router_operations after indexer response is received
  • Derived sequence is compared against displayed intermediate_tokens before submission
  • Mismatch causes route rejection with a user-visible error
  • Frontend test asserts that a tampered intermediate token sequence is caught before submission

Cc : @PlasticDigits

### Summary When the frontend displays a multihop route to the user (e.g. LUNC -\> tokenX -\> USTC), the intermediate token addresses come from the indexer response. The actual swap is built from the same indexer response's router_operations field. The frontend does not cross-validate that the intermediate token addresses displayed to the user match the token addresses in the submitted router_operations. A malicious indexer can display one route and submit a different token path, routing through unintended intermediate tokens within the user's slippage tolerance. This is related to Issue (#449) but affects the intermediate path rather than the terminal pair contract. Together they represent the full trust-the-indexer attack surface for route integrity. --- ### What Was Checked - `frontend-dapp/src/utils/swapRouteDisplay.ts` lines 65-110: `computeSwapRouteDisplay()` derives the displayed route from indexer operations. - `frontend-dapp/src/utils/swapRouteDisplay.ts` lines 115-131: `deriveSwapSubmitRouteSource()` uses indexer operations for submission without re-deriving expected intermediate tokens from router_operations to compare against the displayed path. - `frontend-dapp/src/pages/SwapPage.tsx` line \~531: only token_in and token_out are validated against the user's selected tokens. Intermediate token addresses in router_operations are accepted as-is from the indexer response. - `frontend-dapp/src/services/terraclassic/swapRoutePreflight.ts`: enrichment function adds min_return per hop but does not validate intermediate token addresses. --- ### Expected (per checklist) Before submitting a multihop swap, the frontend re-derives expected intermediate token addresses from the selected token pair and the number of hops, then confirms each hop's offer_asset and ask_asset in router_operations match the expected path. Any mismatch causes submission to be blocked with a clear error. Maps to SEC-E07 (route display aligns with actual submit operations). --- ### Actual No intermediate token address validation exists. The displayed path and submitted operations are both derived from the indexer response with no cross-check. --- ### Suggested Fix After receiving and accepting the indexer route response, extract the actual intermediate token sequence from router_operations (each hop's ask_asset_info becomes the next hop's offer_asset_info). Compare this derived sequence against the displayed intermediate_tokens array. If they do not match, reject the route and log a warning. This check runs client-side before enrichment and submission. --- ### Verification Checklist - [ ] Frontend derives intermediate token sequence from router_operations after indexer response is received - [ ] Derived sequence is compared against displayed intermediate_tokens before submission - [ ] Mismatch causes route rejection with a user-visible error - [ ] Frontend test asserts that a tampered intermediate token sequence is caught before submission Cc : @PlasticDigits
totdking commented 2026-06-30 18:37:55 +00:00 (Migrated from gitlab.com)

mentioned in issue #381

mentioned in issue #381
Brouie commented 2026-06-30 19:13:34 +00:00 (Migrated from gitlab.com)

mentioned in merge request !987

mentioned in merge request !987
Brouie commented 2026-06-30 19:13:44 +00:00 (Migrated from gitlab.com)

Took the logic half of this. The displayed route (intermediate_tokens) and the submitted ops (router_operations) both come from the indexer with no cross-check, so a tampered indexer could show one path and submit another inside slippage tolerance.

Added swapRouteIntermediateTokensAligned(operations, intermediateTokens) in swapRouteDisplay.ts — it re-derives the token path from the operations actually being submitted and compares it against the displayed intermediate tokens (case-insensitive, address-normalized, since the address is the trust anchor, not the symbol). Wired it into the route-solve quote in SwapPage: a mismatch throws, so the quote falls through to the pool-only path instead of signing an indexer-substituted route. token_in/token_out are already matched separately, so this closes the intermediate/terminal-hop gap.

Tests (new, swapRouteIntermediateTokensAligned describe block): aligned passes, case-insensitive passes, substituted intermediate / substituted terminal / length-mismatch all rejected, and the nothing-to-cross-check cases pass. swapRouteDisplay 11/0, SwapPage suites 32/0, tsc clean.

Verified at the source + unit layer here. The user-facing behavior (a tampered route surfaces cleanly in the dapp rather than just falling back) is a browser-walkthrough item — flagging that for the laptop UI pass.

This is closely related to #449 (terminal pair-contract display); I'm treating the signing-modal pair-address display there as the remaining UI-layer piece.

MR !987, branch qa/450-route-intermediate-token-crosscheck, commit 6b53f27f. Needs review/merge @PlasticDigits — leaving open for verification.

Took the logic half of this. The displayed route (`intermediate_tokens`) and the submitted ops (`router_operations`) both come from the indexer with no cross-check, so a tampered indexer could show one path and submit another inside slippage tolerance. Added `swapRouteIntermediateTokensAligned(operations, intermediateTokens)` in `swapRouteDisplay.ts` — it re-derives the token path from the operations actually being submitted and compares it against the displayed intermediate tokens (case-insensitive, address-normalized, since the address is the trust anchor, not the symbol). Wired it into the route-solve quote in `SwapPage`: a mismatch throws, so the quote falls through to the pool-only path instead of signing an indexer-substituted route. token_in/token_out are already matched separately, so this closes the intermediate/terminal-hop gap. Tests (new, `swapRouteIntermediateTokensAligned` describe block): aligned passes, case-insensitive passes, substituted intermediate / substituted terminal / length-mismatch all rejected, and the nothing-to-cross-check cases pass. swapRouteDisplay 11/0, SwapPage suites 32/0, tsc clean. Verified at the source + unit layer here. The user-facing behavior (a tampered route surfaces cleanly in the dapp rather than just falling back) is a browser-walkthrough item — flagging that for the laptop UI pass. This is closely related to #449 (terminal pair-contract display); I'm treating the signing-modal pair-address display there as the remaining UI-layer piece. MR !987, branch `qa/450-route-intermediate-token-crosscheck`, commit `6b53f27f`. Needs review/merge @PlasticDigits — leaving open for verification.
Brouie commented 2026-06-30 19:15:32 +00:00 (Migrated from gitlab.com)

mentioned in issue #449

mentioned in issue #449
PlasticDigits commented 2026-06-30 22:03:54 +00:00 (Migrated from gitlab.com)

mentioned in commit 2ae3372716

mentioned in commit 2ae337271615da6c54255738fa15b3b8f47716d3
PlasticDigits commented 2026-06-30 22:04:47 +00:00 (Migrated from gitlab.com)

Fall thru is unacceptable, instead if there is a conflict the route should be updated and user notified.

Fall thru is unacceptable, instead if there is a conflict the route should be updated and user notified.
PlasticDigits commented 2026-06-30 22:20:08 +00:00 (Migrated from gitlab.com)

mentioned in commit 673446b23c

mentioned in commit 673446b23cbf38d93fc24a44a9cba3850062330f
PlasticDigits commented 2026-06-30 22:20:19 +00:00 (Migrated from gitlab.com)

mentioned in merge request !994

mentioned in merge request !994
PlasticDigits commented 2026-07-01 00:39:27 +00:00 (Migrated from gitlab.com)

mentioned in commit f7732c6706

mentioned in commit f7732c6706e09f43fac0da51727be373cc253ea1
PlasticDigits commented 2026-07-01 00:46:52 +00:00 (Migrated from gitlab.com)

Verification — SEC-I02 (H09) / #450

Verified on main at 772a7dec (includes merge f7732c67 — reconcile display + notify on mismatch per maintainer feedback).

Acceptance checklist

Criterion Result How verified
Frontend derives intermediate token sequence from router_operations after indexer response PASS tokenPathFromSwapOperations() in swapRouteDisplay.ts; used by reconcileSwapRouteIntermediateTokens() wired in SwapPage.tsx quote path (~L546–574).
Derived sequence compared against displayed intermediate_tokens before submit PASS swapRouteIntermediateTokensAligned() + reconcileSwapRouteIntermediateTokens() run on every indexer multihop quote before indexerOperations / display tokens are stored.
Mismatch surfaced to user (no silent pool-only / client-BFS fallback) PASS On mismatch: display reconciled to ops path, indexerRouteIntermediateReconciled flag set, swap-route-intermediate-reconciled shows "Route adjusted." Submit still uses indexer router_operations (execution truth). Aligns with @PlasticDigits comment (reconcile + notify, not fall-through).
Frontend test catches tampered intermediate path PASS swapRouteDisplay.test.ts — swapRouteIntermediateTokensAligned + reconcileSwapRouteIntermediateTokens (aligned, case-insensitive, substituted intermediate/terminal, length mismatch). SwapPage.test.tsx — reconciles tampered indexer intermediate_tokens to ops path and notifies user (#450 / SEC-I02 H09).

Automated runs

npm test -- --run src/utils/swapRouteDisplay.test.ts src/pages/SwapPage.test.tsx
→ 45/45 passed (swapRouteDisplay 15, SwapPage 30 incl. #450 case)

npx tsc --noEmit (frontend-dapp)
→ exit 0

Manual / UI (skill checklist item 6)

PASS (via integration test — same DOM assertions as browser walkthrough): tampered intermediate_tokens → route row shows ops path (terraEvil, not terraB), swap-route-intermediate-reconciled visible with "route adjusted", no swap-route-source-client-fallback.

Docs cross-check

  • skills/AGENTS_FRONTEND_SWAP_ROUTE_DISPLAY.md — #450 reconciliation invariant documented.
  • docs/security-model.md — indexer trust boundary updated for reconcile + warn.

All criteria pass. Closing #450.

## Verification — SEC-I02 (H09) / #450 Verified on `main` at `772a7dec` (includes merge `f7732c67` — reconcile display + notify on mismatch per maintainer feedback). ### Acceptance checklist | Criterion | Result | How verified | |-----------|--------|--------------| | Frontend derives intermediate token sequence from `router_operations` after indexer response | **PASS** | `tokenPathFromSwapOperations()` in `swapRouteDisplay.ts`; used by `reconcileSwapRouteIntermediateTokens()` wired in `SwapPage.tsx` quote path (~L546–574). | | Derived sequence compared against displayed `intermediate_tokens` before submit | **PASS** | `swapRouteIntermediateTokensAligned()` + `reconcileSwapRouteIntermediateTokens()` run on every indexer multihop quote before `indexerOperations` / display tokens are stored. | | Mismatch surfaced to user (no silent pool-only / client-BFS fallback) | **PASS** | On mismatch: display reconciled to ops path, `indexerRouteIntermediateReconciled` flag set, `swap-route-intermediate-reconciled` shows **"Route adjusted."** Submit still uses indexer `router_operations` (execution truth). Aligns with @PlasticDigits comment (reconcile + notify, not fall-through). | | Frontend test catches tampered intermediate path | **PASS** | `swapRouteDisplay.test.ts` — `swapRouteIntermediateTokensAligned` + `reconcileSwapRouteIntermediateTokens` (aligned, case-insensitive, substituted intermediate/terminal, length mismatch). `SwapPage.test.tsx` — `reconciles tampered indexer intermediate_tokens to ops path and notifies user (#450 / SEC-I02 H09)`. | ### Automated runs ``` npm test -- --run src/utils/swapRouteDisplay.test.ts src/pages/SwapPage.test.tsx → 45/45 passed (swapRouteDisplay 15, SwapPage 30 incl. #450 case) npx tsc --noEmit (frontend-dapp) → exit 0 ``` ### Manual / UI (skill checklist item 6) **PASS** (via integration test — same DOM assertions as browser walkthrough): tampered `intermediate_tokens` → route row shows ops path (`terraEvil`, not `terraB`), `swap-route-intermediate-reconciled` visible with "route adjusted", no `swap-route-source-client-fallback`. ### Docs cross-check - `skills/AGENTS_FRONTEND_SWAP_ROUTE_DISPLAY.md` — #450 reconciliation invariant documented. - `docs/security-model.md` — indexer trust boundary updated for reconcile + warn. All criteria pass. Closing #450.
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-07-01 00:46:53 +00:00
Brouie commented 2026-07-01 11:19:30 +00:00 (Migrated from gitlab.com)

mentioned in issue #447

mentioned in issue #447
Brouie commented 2026-07-01 12:57:10 +00:00 (Migrated from gitlab.com)

mentioned in issue #471

mentioned in issue #471
totdking commented 2026-07-02 16:13:11 +00:00 (Migrated from gitlab.com)

mentioned in commit 9ff4ea7b60

mentioned in commit 9ff4ea7b6091af242260e3566c86b126f590411c
totdking commented 2026-07-02 16:14:08 +00:00 (Migrated from gitlab.com)

mentioned in merge request !1007

mentioned in merge request !1007
PlasticDigits commented 2026-08-27 00:20:45 +00:00 (Migrated from gitlab.com)

mentioned in issue #679

mentioned in issue #679
PlasticDigits commented 2026-08-27 00:20:56 +00:00 (Migrated from gitlab.com)

mentioned in issue #681

mentioned in issue #681
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-dex-terraclassic#450
No description provided.