From 1d494689204c90c5ebe6f1e509fcb54c475bfc26 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sun, 13 Sep 2026 12:42:27 +0300 Subject: [PATCH] =?UTF-8?q?docs(security):=20=D0=BD=D1=83=D0=BC=D0=B5?= =?UTF-8?q?=D1=80=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BF=D1=83=D0=BD=D0=BA=D1=82?= =?UTF-8?q?=D0=BE=D0=B2=20=D1=87=D0=B5=D0=BA-=D0=BB=D0=B8=D1=81=D1=82?= =?UTF-8?q?=D0=B0=20SEC-1..SEC-15?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Сквозные номера не сбиваются при добавлении/закрытии пунктов внутри разделов. В SEC-4 фикс ссылается на SEC-1/SEC-2. В сводке — раскладка по важности. --- README.md | 8 ++++---- TASKS.md | 2 +- docs/SECURITY.md | 36 ++++++++++++++++++++---------------- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 4628738..89857a6 100644 --- a/README.md +++ b/README.md @@ -67,10 +67,10 @@ binance-market — это **не часть игры**, а **общий инфр ## Безопасность -**Открытый чек-лист — `docs/SECURITY.md`** (15 пунктов: 6 критичных, 3 по экономике, 3 по инфраструктуре, 3 отложенных; аудит живого стенда 2026-09-13). -Критично на сегодня: у контракта нет верхней границы окна закрытия ставки и нет серверного -авто-закрытия — момент расчёта выбирает клиент, что даёт безрисковый арбитраж. Плюс открытые -наружу `/bet`, `/close`, `/faucet` без авторизации. +**Открытый чек-лист — `docs/SECURITY.md`** (пункты `SEC-1…SEC-15`, аудит живого стенда 2026-09-13). +Критично на сегодня (`SEC-1…SEC-6`): у контракта нет верхней границы окна закрытия ставки и нет +серверного авто-закрытия — момент расчёта выбирает клиент, что даёт безрисковый арбитраж. Плюс +открытые наружу `/bet`, `/close`, `/faucet` без авторизации. ## Прочее - Ночная экономика и mint-адреса: `docs/ECONOMY.md` (создаётся в Фазе 0). diff --git a/TASKS.md b/TASKS.md index fe82679..d5bb718 100644 --- a/TASKS.md +++ b/TASKS.md @@ -105,4 +105,4 @@ ## Ссылки (дополнено 2026-09-13) - Цена: `docs/PRICE-NATS.md`, диаграмма `docs/diagrams/price-flow.puml` -- **Безопасность: `docs/SECURITY.md` — чек-лист (15 открытых пунктов, аудит живого стенда 2026-09-13)** +- **Безопасность: `docs/SECURITY.md` — чек-лист `SEC-1…SEC-15` (аудит живого стенда 2026-09-13)** diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 77b286d..b003e70 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -7,36 +7,40 @@ Аудит проведён **на живом стенде**, не по чтению кода: воспроизведение эксплойта, проверка ручек из внешнего интернета, проверка аутентификации шины. Фактические выводы приведены в строках. -Отмечать закрытое — `[ ]` → `[x]`; в закрытую строку добавлять дату и коммит/релиз. +Пункты нумеруются сквозным `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. --- ## Критично — эксплуатируется -- [ ] **Нет верхней границы окна закрытия** — `close_bet` проверяет только `now >= expire_time`, но не `now <= expire_time + WINDOW`. Ставка остаётся `open` бесконечно, и момент расчёта выбирает тот, кто позвал `/close`. Место: `program/programs/smart-updown/src/instructions/close_bet.rs:60`. Фикс: `require!(now <= bet.expire_time + CLOSE_WINDOW_SECONDS, Error::CloseWindowExpired)`. -- [ ] **Нет авто-закрытия на сервере** — в шлюзе отсутствует таймер закрытия ставок; единственный `setInterval` в коде — WS-heartbeat. Экспирация ничего не значит, раунд завершает клиент. Место: `relayer/src/gateway.ts:621`. Фикс: серверный планировщик, закрывающий ставку в момент `expire_time`. -- [ ] **`POST /close` доступен из интернета без авторизации** — любой может закрыть чужую ставку в выбранный им момент. Проверено: `curl -X POST https://testfront.binom.pw/api/close -d '{"id":"1"}'` → обработчик выполняется. Фикс: авторизация + серверное закрытие. -- [ ] **Безрисковый арбитраж на паре ставок UP+DOWN** — при `entry ≈ X` обе ставки закрываются в свой выгодный момент, обе получают `payout_done` (пара как схема — **вывод** из дыры окна; прямого прогона пары не делали). Прямо воспроизведено: ставка №5 (`DOWN`, `entry=9998000000`) висела `open` >2 мин после экспирации 15 с и закрыта при `exitPrice=9997000000` → `{"status":1,"statusName":"payout_done"}` — момент расчёта выбран вручную. Фикс: закрытие окна + серверное закрытие. -- [ ] **`POST /bet` без авторизации** — любой может создавать ставки на произвольные адреса и накручивать счётчик раундов `global.counter`. Проверено: `curl -X POST .../api/bet` → 200. Фикс: токен/подпись запроса. -- [ ] **`GET /faucet/{address}` открыт наружу и выдаёт реальные средства** — 5 SOL + 10 000 фантиков на любой адрес. Проверено: `curl https://testfront.binom.pw/api/faucet/6Q1ko…` → `{"sol":5,"solLamports":5000000000,"tokenUnits":"10000000000","tokenHuman":"10000.000000"}`. Гейт `SMART_ALLOW_AIRDROP=1` выставлен в боевом поде. Фикс: `SMART_ALLOW_AIRDROP=0` вне localnet, ручку не публиковать наружу. +- [ ] **SEC-1. Нет верхней границы окна закрытия ставки** — `close_bet` проверяет только `now >= expire_time`, но не `now <= expire_time + WINDOW`. Ставка остаётся `open` бесконечно, и момент расчёта выбирает тот, кто позвал `/close`. Место: `program/programs/smart-updown/src/instructions/close_bet.rs:60`. Фикс: `require!(now <= bet.expire_time + CLOSE_WINDOW_SECONDS, Error::CloseWindowExpired)`. +- [ ] **SEC-2. Нет авто-закрытия ставок на сервере** — в шлюзе отсутствует таймер закрытия; единственный `setInterval` в коде — WS-heartbeat. Экспирация ничего не значит, раунд завершает клиент. Место: `relayer/src/gateway.ts:621`. Фикс: серверный планировщик, закрывающий ставку в момент `expire_time`. +- [ ] **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}` открыт наружу и выдаёт реальные средства** — 5 SOL + 10 000 фантиков на любой адрес. Проверено: `curl https://testfront.binom.pw/api/faucet/6Q1ko…` → `{"sol":5,"solLamports":5000000000,"tokenUnits":"10000000000","tokenHuman":"10000.000000"}`. Гейт `SMART_ALLOW_AIRDROP=1` выставлен в боевом поде. Фикс: `SMART_ALLOW_AIRDROP=0` вне localnet, ручку не публиковать наружу. ## Важно — экономика и устойчивость -- [ ] **Множитель 1.3× требует от дома винрейта ~77 %** — `SMART_MULTIPLIER_BPS=13000`; порог безубытка = 1/1.3 = 76.9 %. При честной монете дом убыточен, а с дырой окна закрытия игрок забирает близко к 100 %. Фикс: пересчитать экономику под реальный edge. -- [ ] **Нефинализированные ставки запирают средства в ваулте** — `vault` не уменьшается, пока ставку не закроют. Наблюдение: `vaultBalanceHuman` 28100 → 30100 при незакрытых ставках. Фикс: серверное закрытие + клинап просроченных. -- [ ] **Равные `exit_price == entry_price` не финализируют ставку** — `close_bet` делает `return Ok(())`, статус остаётся `open`; при «плоской» цене ставка зависает без исхода. Место: `close_bet.rs:66`. Фикс: определить политику (продление / возврат / ничья) вместо молчаливого выхода. +- [ ] **SEC-7. Множитель 1.3× требует от дома винрейта ~77 %** — `SMART_MULTIPLIER_BPS=13000`; порог безубытка = 1/1.3 = 76.9 %. При честной монете дом убыточен, а с дырой окна закрытия игрок забирает близко к 100 %. Фикс: пересчитать экономику под реальный edge. +- [ ] **SEC-8. Нефинализированные ставки запирают средства в ваулте** — `vault` не уменьшается, пока ставку не закроют. Наблюдение: `vaultBalanceHuman` 28100 → 30100 при незакрытых ставках. Фикс: серверное закрытие + клинап просроченных. +- [ ] **SEC-9. Равные `exit_price == entry_price` не финализируют ставку** — `close_bet` делает `return Ok(())`, статус остаётся `open`; при «плоской» цене ставка зависает без исхода. Место: `close_bet.rs:66`. Фикс: определить политику (продление / возврат / ничья) вместо молчаливого выхода. ## Средний приоритет — инфраструктура -- [ ] **NATS без аутентификации** — `auth_required` не выставлен, анонимная публикация в шину проходит (проверено на тестовом subject; боевой не трогали). Пока цена условна — терпимо; как только станет денежной, у шины нет прав на запись и любой сможет напечатать свой курс. Фикс: auth + subject-ACL, шлюзу — read-only на `market.price.*`. -- [ ] **Ключ релейера — единственная точка доверия в контракте** — контракт не проверяет соответствие цены рынку и принимает любое значение от подписанта (`create_bet.rs:75`, `close_bet.rs:62`). Модель «релейер = оракул» стандартна, но компрометация одного ключа `E7Hg…` = произвольный исход всех ставок. Фикс: on-chain оракул (Pyth/Switchboard) для боя. -- [ ] **`entry_price` не проверяется контрактом** — передаётся в `create_bet` целиком на усмотрение вызывающего, контракт не может убедиться, что это цена момента `bet_time`. Место: `create_bet.rs:46,75`. Фикс: внешний верифицируемый фид. +- [ ] **SEC-10. NATS без аутентификации** — `auth_required` не выставлен, анонимная публикация в шину проходит (проверено на тестовом subject; боевой не трогали). Пока цена условна — терпимо; как только станет денежной, у шины нет прав на запись и любой сможет напечатать свой курс. Фикс: auth + subject-ACL, шлюзу — read-only на `market.price.*`. +- [ ] **SEC-11. Ключ релейера — единственная точка доверия в контракте** — контракт не проверяет соответствие цены рынку и принимает любое значение от подписанта (`create_bet.rs:75`, `close_bet.rs:62`). Модель «релейер = оракул» стандартна, но компрометация одного ключа `E7Hg…` = произвольный исход всех ставок. Фикс: on-chain оракул (Pyth/Switchboard) для боя. +- [ ] **SEC-12. `entry_price` не проверяется контрактом** — передаётся в `create_bet` целиком на усмотрение вызывающего; контракт не может убедиться, что это цена момента `bet_time`. Место: `create_bet.rs:46,75`. Фикс: внешний верифицируемый фид. ## Отложено — зафиксировать как риск -- [ ] **`SMART_ALLOW_AIRDROP=1` задеплоен в боевом поде** — флаг «только localnet» фактически включён на публичном стенде. Проверка: `kubectl get pod -n game … -o jsonpath='{.spec.containers[0].env}'`. Фикс: per-environment values, по умолчанию `0`. -- [ ] **Политика деградации при отсутствии цены не определена** — сейчас таймаут `PRICE_WAIT_TIMEOUT_MS=15000`, ставки просто падают. Место: `relayer/src/priceSource.ts:9`. Нужно решение: останавливать приём ставок / бэкфилл / заморозка раундов. -- [ ] **Аудит `binance-market` (upstream цены) не проводился** — по договорённости источник считается доверенным. Репо `subochev/binance-market`, ns `invest`. Проверить авторизацию публикации в NATS и целостность источника. +- [ ] **SEC-13. `SMART_ALLOW_AIRDROP=1` задеплоен в боевом поде** — флаг «только localnet» фактически включён на публичном стенде. Проверка: `kubectl get pod -n game … -o jsonpath='{.spec.containers[0].env}'`. Фикс: per-environment values, по умолчанию `0`. +- [ ] **SEC-14. Политика деградации при отсутствии цены не определена** — сейчас таймаут `PRICE_WAIT_TIMEOUT_MS=15000`, ставки просто падают. Место: `relayer/src/priceSource.ts:9`. Нужно решение: останавливать приём ставок / бэкфилл / заморозка раундов. +- [ ] **SEC-15. Аудит `binance-market` (upstream цены) не проводился** — по договорённости источник считается доверенным. Репо `subochev/binance-market`, ns `invest`. Проверить авторизацию публикации в NATS и целостность источника. ---