cg/cmc orderbook endpoints bypass the lcd-heavy rate limiter #278

Closed
opened 2026-06-03 07:12:03 +00:00 by Brouie · 18 comments
Brouie commented 2026-06-03 07:12:03 +00:00 (Migrated from gitlab.com)

Severity: High
Reachability: Unauthenticated HTTP. Any client.
Affected: /cg/orderbook and /cmc/orderbook/{market_pair} route placement (indexer/src/api/mod.rs).
Root cause: the CG/CMC orderbook endpoints serve the same LCD-heavy orderbook data as the native book endpoints, but they're mounted on api_router instead of lcd_heavy_router, so they skip the stricter per-IP rate limit.

Summary

The native book endpoints — order-book-head, limit-book, limit-book-shallow, insert-hints — and the solver are deliberately put behind lcd_heavy_router with rate_limit_lcd_heavy_rps, because each request fans out a pile of synchronous LCD queries. But /cg/orderbook and /cmc/orderbook/{market_pair} build the same kind of orderbook snapshot and are registered straight on api_router, under only the general limiter.

So an attacker just hits the CG/CMC orderbook mirrors instead of the native ones to drive the same LCD amplification while bypassing the throttle that was added specifically to contain it. It's a hole in an existing control, which is why I'm putting it at High.

Current codebase

  • indexer/src/api/mod.rs: lcd_heavy_router lists the native book + solve routes and gets apply_rate_limit_layer(..., rate_limit_lcd_heavy_rps). api_router separately registers /cg/orderbook and /cmc/orderbook/{market_pair} and only .merges the heavy router in — the cg/cmc routes are siblings, not members.
  1. Move /cg/orderbook and /cmc/orderbook/{market_pair} into lcd_heavy_router (or apply the same per-IP heavy limit to them).
  2. Audit the rest of the cg/cmc surface for other LCD-fanout handlers that ended up on the general limiter.

Acceptance criteria

  • /cg/orderbook and /cmc/orderbook/* are subject to the same per-IP rate limit as the native book endpoints.
  • No LCD-fanout route is reachable under only the general limiter.

Test plan (attack / abuse)

case expect
hammer /cg/orderbook past lcd-heavy rps throttled the same as /limit-book
hammer /cmc/orderbook/{pair} throttled
**Severity:** High **Reachability:** Unauthenticated HTTP. Any client. **Affected:** `/cg/orderbook` and `/cmc/orderbook/{market_pair}` route placement (`indexer/src/api/mod.rs`). **Root cause:** the CG/CMC orderbook endpoints serve the same LCD-heavy orderbook data as the native book endpoints, but they're mounted on `api_router` instead of `lcd_heavy_router`, so they skip the stricter per-IP rate limit. ## Summary The native book endpoints — `order-book-head`, `limit-book`, `limit-book-shallow`, `insert-hints` — and the solver are deliberately put behind `lcd_heavy_router` with `rate_limit_lcd_heavy_rps`, because each request fans out a pile of synchronous LCD queries. But `/cg/orderbook` and `/cmc/orderbook/{market_pair}` build the same kind of orderbook snapshot and are registered straight on `api_router`, under only the general limiter. So an attacker just hits the CG/CMC orderbook mirrors instead of the native ones to drive the same LCD amplification while bypassing the throttle that was added specifically to contain it. It's a hole in an existing control, which is why I'm putting it at High. ## Current codebase - `indexer/src/api/mod.rs`: `lcd_heavy_router` lists the native book + solve routes and gets `apply_rate_limit_layer(..., rate_limit_lcd_heavy_rps)`. `api_router` separately registers `/cg/orderbook` and `/cmc/orderbook/{market_pair}` and only `.merge`s the heavy router in — the cg/cmc routes are siblings, not members. ## Recommended direction 1. Move `/cg/orderbook` and `/cmc/orderbook/{market_pair}` into `lcd_heavy_router` (or apply the same per-IP heavy limit to them). 2. Audit the rest of the cg/cmc surface for other LCD-fanout handlers that ended up on the general limiter. ## Acceptance criteria - [ ] `/cg/orderbook` and `/cmc/orderbook/*` are subject to the same per-IP rate limit as the native book endpoints. - [ ] No LCD-fanout route is reachable under only the general limiter. ## Test plan (attack / abuse) | case | expect | |---|---| | hammer /cg/orderbook past lcd-heavy rps | throttled the same as /limit-book | | hammer /cmc/orderbook/{pair} | throttled |
PlasticDigits commented 2026-06-03 10:38:01 +00:00 (Migrated from gitlab.com)

Move both routes to the lcd_heavy_router

Move both routes to the lcd_heavy_router
Brouie commented 2026-06-04 03:10:04 +00:00 (Migrated from gitlab.com)

Fixed — moved /cg/orderbook and /cmc/orderbook/{market_pair} into lcd_heavy_router (indexer/src/api/mod.rs) so they inherit the same rate_limit_lcd_heavy_rps per-IP throttle as the native limit-book / route-solve endpoints. Both handlers run the same orderbook_sim::simulate_orderbook_cached LCD fanout (resolve_fee_bps + build_orderbook_data), so they belong under the heavy limiter, not the general one. cargo check clean.

Note on AC item 2 ("no LCD-fanout route reachable under only the general limiter"): that's broader than this move — the cg/cmc tickers/summary N+1 fanout is the separate #288 (TTL + pagination), so I scoped this MR to the orderbook routes per your "move both routes to the lcd_heavy_router" note and left the tickers/summary surface to #288.

Branch qa/278-cgcmc-orderbook-lcd-heavy, MR fork→main (no closing keyword). Live per-IP throttle verification rides on the indexer running on v4, which is gated on the indexer tx-search fix (#292 / MR !738) — until that merges the indexer can't sync on the new stack. @PlasticDigits

Fixed — moved `/cg/orderbook` and `/cmc/orderbook/{market_pair}` into `lcd_heavy_router` (indexer/src/api/mod.rs) so they inherit the same `rate_limit_lcd_heavy_rps` per-IP throttle as the native limit-book / route-solve endpoints. Both handlers run the same `orderbook_sim::simulate_orderbook_cached` LCD fanout (resolve_fee_bps + build_orderbook_data), so they belong under the heavy limiter, not the general one. cargo check clean. Note on AC item 2 ("no LCD-fanout route reachable under only the general limiter"): that's broader than this move — the cg/cmc **tickers/summary** N+1 fanout is the separate #288 (TTL + pagination), so I scoped this MR to the orderbook routes per your "move both routes to the lcd_heavy_router" note and left the tickers/summary surface to #288. Branch `qa/278-cgcmc-orderbook-lcd-heavy`, MR fork→main (no closing keyword). Live per-IP throttle verification rides on the indexer running on v4, which is gated on the indexer tx-search fix (#292 / MR !738) — until that merges the indexer can't sync on the new stack. @PlasticDigits
Brouie commented 2026-06-04 03:10:06 +00:00 (Migrated from gitlab.com)

mentioned in merge request !739

mentioned in merge request !739
Brouie commented 2026-06-04 06:30:08 +00:00 (Migrated from gitlab.com)

mentioned in issue #282

mentioned in issue #282
PlasticDigits commented 2026-06-04 08:02:09 +00:00 (Migrated from gitlab.com)

mentioned in commit 03e6d7c88e

mentioned in commit 03e6d7c88e163dc79f1b29d39c84b898767eb409
Brouie commented 2026-06-05 01:21:09 +00:00 (Migrated from gitlab.com)

Verified the orderbook routes are under the heavy limiter, and I re-ran the live per-IP throttle check that was blocked last session on the v4 indexer sync — it's unblocked now, so this is end-to-end confirmed.

AC1 (cg/cmc orderbook under the same per-IP limit as the native book): both /cg/orderbook and /cmc/orderbook/{market_pair} are inside lcd_heavy_router now (api/mod.rs), behind the same rate_limit_lcd_heavy_rps layer as order-book-head / limit-book / route-solve. Live: a burst on /cg/orderbook runs ~20x 200 then flips to 429, and the 429 carries x-ratelimit-limit: 20 (rps 10 x burst 2) — identical to the native order-book-head budget; /cmc/orderbook shares the same per-IP heavy bucket (it 429s on the same burst once the budget is drained). A general-limiter route (/api/v1/overview) under the same hammering stays 200 across the board, so the heavy budget is really what's gating these now, not the general one. Bucket refills per second.

AC2 (no LCD-fanout route left on only the general limiter): closed for the orderbook surface here. The other LCD-fanout endpoints — cg/cmc tickers + summary — are the N+1 in #288 where I put the cache; I scoped this one to the orderbook routes per your "move both routes" note.

@PlasticDigits good to close.

Verified the orderbook routes are under the heavy limiter, and I re-ran the live per-IP throttle check that was blocked last session on the v4 indexer sync — it's unblocked now, so this is end-to-end confirmed. AC1 (cg/cmc orderbook under the same per-IP limit as the native book): both `/cg/orderbook` and `/cmc/orderbook/{market_pair}` are inside `lcd_heavy_router` now (api/mod.rs), behind the same `rate_limit_lcd_heavy_rps` layer as `order-book-head` / `limit-book` / route-solve. Live: a burst on `/cg/orderbook` runs ~20x 200 then flips to 429, and the 429 carries `x-ratelimit-limit: 20` (rps 10 x burst 2) — identical to the native `order-book-head` budget; `/cmc/orderbook` shares the same per-IP heavy bucket (it 429s on the same burst once the budget is drained). A general-limiter route (`/api/v1/overview`) under the same hammering stays 200 across the board, so the heavy budget is really what's gating these now, not the general one. Bucket refills per second. AC2 (no LCD-fanout route left on only the general limiter): closed for the orderbook surface here. The other LCD-fanout endpoints — cg/cmc tickers + summary — are the N+1 in #288 where I put the cache; I scoped this one to the orderbook routes per your "move both routes" note. @PlasticDigits good to close.
ghost1 commented 2026-06-05 04:04:53 +00:00 (Migrated from gitlab.com)

mentioned in commit 9e47f4daf7

mentioned in commit 9e47f4daf74f9cba057c9517056db3f3deb20e63
PlasticDigits commented 2026-06-05 04:05:17 +00:00 (Migrated from gitlab.com)

mentioned in merge request !765

mentioned in merge request !765
PlasticDigits commented 2026-06-05 04:15:12 +00:00 (Migrated from gitlab.com)

mentioned in commit bd90d82902

mentioned in commit bd90d82902ab4ff4b08cdad655731b1d8a5e52bd
PlasticDigits commented 2026-06-05 04:19:52 +00:00 (Migrated from gitlab.com)

mentioned in issue #319

mentioned in issue #319
Brouie commented 2026-06-05 06:59:33 +00:00 (Migrated from gitlab.com)

mentioned in merge request !776

mentioned in merge request !776
PlasticDigits commented 2026-06-05 09:41:53 +00:00 (Migrated from gitlab.com)

Verification — issue #278 (agent:verify)

Issue: https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/278
Branch verified: main @ 4f8019c / 9e47f4d (fix + regression test already merged)

Acceptance criteria

Item Result How verified
AC1 — /cg/orderbook and /cmc/orderbook/* use the same per-IP LCD-heavy limit as native book routes PASS indexer/src/api/mod.rs: both routes registered on lcd_heavy_router before apply_rate_limit_layer(..., rate_limit_lcd_heavy_rps). Not duplicated on api_router (comment at merge site).
AC2 — No LCD-fanout route reachable under only the general limiter PASS (orderbook surface) CG/CMC orderbook handlers are the only CG/CMC paths that call orderbook_sim::simulate_orderbook_cached / LCD. Other CG/CMC listing endpoints (/cg/tickers, /cmc/summary, etc.) are DB N+1 + 60s aggregator cache (#288), not LCD fanout — out of scope for this issue per maintainer note.

Automated tests

cd indexer && cargo test -j1 --test security lcd_heavy -- --test-threads=1
# cg_cmc_orderbook_lcd_heavy_rate_limit_returns_429 ... ok
# lcd_heavy_route_rate_limit_returns_429 ... ok

cd indexer && cargo test -j1 --test security rate_limit -- --test-threads=1
# (above + rate_limit_returns_429_when_exceeded) — 3/3 ok

cg_cmc_orderbook_lcd_heavy_rate_limit_returns_429 hammers /cg/orderbook and /cmc/orderbook/LUNC_USTC with rate_limit_rps=0, rate_limit_lcd_heavy_rps=5 and asserts 429 — same pattern as native order-book-head.

Manual / live throttle

SKIP — indexer not running on this VM (curl :3001/health → down). Prior implementer comment documents live burst → 429 with x-ratelimit-limit: 20 (10 RPS × burst 2), shared bucket with native book; not re-run here.

Test plan (issue table)

Case Result
Hammer /cg/orderbook past lcd-heavy RPS PASS (automated security.rs)
Hammer /cmc/orderbook/{pair} PASS (same test)

Docs / invariants

docs/indexer-invariants.md, skills/AGENTS_INDEXER_API_LCD_SECURITY.md already list CG/CMC orderbook under LCD-heavy routes — no doc drift found.

Outcome: Criteria met on main; no repo changes from this verification run.

## Verification — issue #278 (agent:verify) **Issue:** https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/278 **Branch verified:** `main` @ `4f8019c` / `9e47f4d` (fix + regression test already merged) ### Acceptance criteria | Item | Result | How verified | |------|--------|--------------| | **AC1** — `/cg/orderbook` and `/cmc/orderbook/*` use the same per-IP LCD-heavy limit as native book routes | **PASS** | `indexer/src/api/mod.rs`: both routes registered on `lcd_heavy_router` before `apply_rate_limit_layer(..., rate_limit_lcd_heavy_rps)`. Not duplicated on `api_router` (comment at merge site). | | **AC2** — No LCD-fanout route reachable under only the general limiter | **PASS** (orderbook surface) | CG/CMC orderbook handlers are the only CG/CMC paths that call `orderbook_sim::simulate_orderbook_cached` / LCD. Other CG/CMC listing endpoints (`/cg/tickers`, `/cmc/summary`, etc.) are DB N+1 + 60s aggregator cache ([#288](https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/288)), not LCD fanout — out of scope for this issue per maintainer note. | ### Automated tests ```text cd indexer && cargo test -j1 --test security lcd_heavy -- --test-threads=1 # cg_cmc_orderbook_lcd_heavy_rate_limit_returns_429 ... ok # lcd_heavy_route_rate_limit_returns_429 ... ok cd indexer && cargo test -j1 --test security rate_limit -- --test-threads=1 # (above + rate_limit_returns_429_when_exceeded) — 3/3 ok ``` `cg_cmc_orderbook_lcd_heavy_rate_limit_returns_429` hammers `/cg/orderbook` and `/cmc/orderbook/LUNC_USTC` with `rate_limit_rps=0`, `rate_limit_lcd_heavy_rps=5` and asserts **429** — same pattern as native `order-book-head`. ### Manual / live throttle **SKIP** — indexer not running on this VM (`curl :3001/health` → down). Prior implementer comment documents live burst → **429** with `x-ratelimit-limit: 20` (10 RPS × burst 2), shared bucket with native book; not re-run here. ### Test plan (issue table) | Case | Result | |------|--------| | Hammer `/cg/orderbook` past lcd-heavy RPS | **PASS** (automated `security.rs`) | | Hammer `/cmc/orderbook/{pair}` | **PASS** (same test) | ### Docs / invariants `docs/indexer-invariants.md`, `skills/AGENTS_INDEXER_API_LCD_SECURITY.md` already list CG/CMC orderbook under LCD-heavy routes — no doc drift found. **Outcome:** Criteria met on `main`; no repo changes from this verification run.
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-06-05 09:41:57 +00:00
PlasticDigits commented 2026-06-05 09:55:26 +00:00 (Migrated from gitlab.com)

mentioned in merge request !783

mentioned in merge request !783
PlasticDigits commented 2026-06-07 12:14:16 +00:00 (Migrated from gitlab.com)

mentioned in issue #337

mentioned in issue #337
PlasticDigits commented 2026-06-12 05:05:45 +00:00 (Migrated from gitlab.com)

mentioned in issue #363

mentioned in issue #363
PlasticDigits commented 2026-06-12 05:06:04 +00:00 (Migrated from gitlab.com)

mentioned in issue #361

mentioned in issue #361
PlasticDigits commented 2026-06-12 05:18:37 +00:00 (Migrated from gitlab.com)

mentioned in merge request !873

mentioned in merge request !873
PlasticDigits commented 2026-08-28 09:24:17 +00:00 (Migrated from gitlab.com)

mentioned in issue #694

mentioned in issue #694
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#278
No description provided.