From 4caddd6a7395e2b3b0fb784274238d9428d6b55a Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 13 Sep 2026 13:39:09 +0300 Subject: [PATCH] =?UTF-8?q?docs(security):=20SEC-6=20=D0=97=D0=90=D0=9A?= =?UTF-8?q?=D0=A0=D0=AB=D0=A2=20=E2=80=94=20=D0=B3=D0=B5=D0=B9=D1=82=20tes?= =?UTF-8?q?t/release=20+=20genesis,=20=D0=BD=D0=B0=D1=87=D0=B8=D1=81=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B2=D1=8B=D0=BD=D0=B5=D1=81=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=20=D0=B8=D0=B7=20/bet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проверено на живом стенде (релизы updown-relayer:2, test-front:3): - /bet без address -> 400, ничего не начисляет - /bet на непополненный кошелёк -> 400 insufficient funds - полный круг POST /wallet -> GET /faucet -> POST /bet -> 200 - smart.mode=release -> 403 'faucet is disabled: only test mode' - npm test 24/24, мутационная приёмка: ослабление проверки кластеров роняет 2 теста Остаточный риск: ручка публично маршрутизируется; в бой — mode=release. --- docs/SECURITY.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/SECURITY.md b/docs/SECURITY.md index f847c2a..94cae10 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -10,8 +10,8 @@ Пункты нумеруются сквозным `SEC-N` (не меняется при добавлении/закрытии пунктов) — ссылаться на них номером. Отмечать закрытое — `[ ]` → `[x]`, в закрытую строку добавлять дату и коммит/релиз. -**Сводка:** открыто 15 — критичных 6 (`SEC-1…SEC-6`), экономика 3 (`SEC-7…SEC-9`), -инфраструктура 3 (`SEC-10…SEC-12`), отложено 3 (`SEC-13…SEC-15`). Закрыто: 0. +**Сводка:** открыто 14 — критичных 5 (`SEC-1…SEC-5`), экономика 3 (`SEC-7…SEC-9`), +инфраструктура 3 (`SEC-10…SEC-12`), отложено 3 (`SEC-13…SEC-15`). Закрыто: 1 (`SEC-6`, 2026-09-13, релизы `updown-relayer:2` / `test-front:3`). --- @@ -22,8 +22,11 @@ - [ ] **SEC-3. `POST /close` доступен из интернета без авторизации** — любой может закрыть чужую ставку в выбранный им момент. Проверено: `curl -X POST https://testfront.binom.pw/api/close -d '{"id":"1"}'` → обработчик выполняется. Фикс: авторизация + серверное закрытие. - [ ] **SEC-4. Безрисковый арбитраж на паре ставок UP+DOWN** — при `entry ≈ X` обе ставки закрываются в свой выгодный момент, обе получают `payout_done` (пара как схема — **вывод** из дыры окна; прямого прогона пары не делали). Прямо воспроизведено: ставка №5 (`DOWN`, `entry=9998000000`) висела `open` >2 мин после экспирации 15 с и закрыта при `exitPrice=9997000000` → `{"status":1,"statusName":"payout_done"}` — момент расчёта выбран вручную. Фикс: `SEC-1` + `SEC-2`. - [ ] **SEC-5. `POST /bet` без авторизации** — любой может создавать ставки на произвольные адреса и накручивать счётчик раундов `global.counter`. Проверено: `curl -X POST .../api/bet` → 200. Фикс: токен/подпись запроса. -- [ ] **SEC-6. `GET /faucet/{address}` открыт наружу и выдаёт реальные средства** — из двух механизмов внутри ручки (`handleFaucet`, `gateway.ts:395`): **(а) SOL-налив** (`fundBettor` → `requestAirdrop`) технически только localnet — mainnet-RPC отвечает `410 Gone "RPC call or parameters have been disabled"` (проверено вживую); **(б) налив фантиков** (`ensureBettorTokenAccount` → `mintTo`) — **НЕ привязан к сети**: работает везде, где у шлюза есть mint authority нашего минта (наш `8dpXNUgW…` — authority `E7Hg…` у релейера, проверено). Гейт — только конфиг-флаг `SMART_ALLOW_AIRDROP`, **проверки кластера в коде нет** (текст «faucet only on localnet» — надпись, не замок). Дополнительно: SOL-барьер обходится — airdrop зовётся лишь при балансе <2 SOL (`ops.ts:204`), значит адрес с ≥2 SOL пролетает airdrop и доходит прямо до `mintTo`. Проверено на стенде: `curl …/api/faucet/6Q1ko…` → `{"sol":5,"solLamports":5000000000,"tokenUnits":"10000000000"}`. Фикс: `SMART_ALLOW_AIRDROP=0` вне localnet (per-environment values), **дефолт в `config.ts` перевернуть на `"0"`** (сейчас `env("SMART_ALLOW_AIRDROP","1")` — открыто по умолчанию), добавить в `handleFaucet` проверку кластера (genesis hash / `getGenesisHash()`), ручку не публиковать наружу. **На боевом мемкойне pump.fun дыра по (б) испаряется сама** — у таких токенов mint authority отзывают; но пока на стенде свой минт с нашей authority — она живая, и полагаться на это нельзя. - *Назначение vs гарантия:* ручка задумана как тестовая (ТЗ `relayer/faucet-TASK.md`: «чтобы Phantom-кошелёк показал баланс > 0») и **на проде не нужна** — там деньги покупаются через swap, а `/bet` фандит беттора сам (`gateway.ts:268-278`), так что даже для тестов ручка почти избыточна (нужна лишь для внешнего Phantom на localnet; `test-front` её не вызывает). Но «не нужна» ≠ «не сработает»: держит её **память деплойщика**, а не код. Наличие ручки в боевом релизе при `allowAirdrop: true` (`helm/values.yaml:22`) означает **работающий публичный кран**, а не безобидный легаси. +- [x] **SEC-6. `GET /faucet/{address}` открыт наружу и выдаёт реальные средства** — **ЗАКРЫТ 2026-09-13** (релизы `updown-relayer:2`, `test-front:3`). Что было: из двух механизмов ручки (`handleFaucet`) — **(а) SOL-налив** (`requestAirdrop`) технически только localnet (mainnet-RPC отдаёт `410 Gone`); **(б) налив фантиков** (`mintTo`) — **НЕ был привязан к сети**: работал везде, где у шлюза есть mint authority минта. Гейтом был только конфиг-флаг `SMART_ALLOW_AIRDROP` с дефолтом **`"1"` (включено)**, проверки кластера в коде не было; SOL-барьер обходился (airdrop звался лишь при балансе <2 SOL, `ops.ts:204`) — адрес с ≥2 SOL доходил прямо до `mintTo`. Было воспроизведено: `curl …/api/faucet/6Q1ko…` → `{"sol":5,"tokenUnits":"10000000000"}` на произвольный адрес без авторизации. + **Что сделано:** (1) новый модуль `relayer/src/faucetGate.ts` — `parseMode` + `faucetDenyReason` с политикой **fail-closed**: отказ, если режим не `test`, если genesis-hash недоступен, или если он принадлежит mainnet-beta/devnet/testnet (`5eykt4…`/`EtWTRA…`/`4uhcVJ…`). (2) `SMART_MODE` (`test|release`), **дефолт `release`** — безопасно забыть, а не вспомнить. (3) `handleFaucet`: 403 на уровне режима и 403 на уровне сети, в лог `faucet DENIED (<причина>)`. (4) Начисление **вынесено из `/bet`** — ставка больше не печатает денег, а `address` стал обязательным; перед ставкой проверяются балансы (SOL ≥ 0.01, фантиков ≥ суммы). (5) Новая ручка `POST /wallet` — создать custodial-кошелёк, **ничего не наливает**. (6) На пульте `test-front` — отдельный блок «Кошелёк и пополнение» с кнопками «Создать кошелёк» / «Пополнить кошелёк». + **Проверено на живом стенде:** `/bet` без адреса → `400 "address is required…"` (и ничего не начисляет); `/bet` на непополненный кошелёк → `400 "insufficient funds…"`; полный круг `POST /wallet` → `GET /faucet` → `POST /bet` → `entryPrice` (HTTP 200); **гейт в обратную сторону: `smart.mode=release` → `403 {"error":"faucet is disabled: only test mode (SMART_MODE=test)"}`** — начисления нет. Тесты: 6 новых проверок гейта, `npm test` 24/24; мутационная приёмка пройдена (ослабление проверки публичных кластеров → падают ровно 2 теста). + **Остаточный риск:** ручка по-прежнему **публично маршрутизируется** (`/api/*` наружу) — гейт теперь в коде, но лучше не выставлять её в интернет. Настройка `smart.mode` в `helm/values.yaml` стенда = `test`; при выкладке в бой **обязательно** `release` (или убрать `/api/faucet` из ингресса). + *Назначение (историческая справка):* ручка задумана как тестовая (ТЗ `relayer/faucet-TASK.md`: «чтобы Phantom-кошелёк показал баланс > 0»), и на проде она не нужна — там деньги покупаются через swap. ## Важно — экономика и устойчивость