# Аудит безопасности — Netrunner Control Plane ## Проход 2026-07-04 — единая авторизация, анти-чит Cyberhack Скоуп: `netrunner-backend` (auth, nodes, users, billing) + `netrunner-backend/frontend` + `netrunner-landing` + `Cyberhack`. | # | Severity | Что | Где | |---|----------|-----|-----| | 1 | **High** | **Сессия нигде реально не работала.** `auth_telegram_api` строил `Set-Cookie`-заголовок, но никогда не прикладывал его к ответу (возвращал голый `Json`, `HeaderMap` отбрасывался); `POST /auth/seed/generate`/`seed/login` вообще не ставили cookie. `frontend/src/api.ts::getTokenCookie()` читал `document.cookie` в расчёте на токен, который сам же сервер помечал `HttpOnly` — то есть прочитать его через JS физически невозможно. Итог: билинг, игра и админка в ЛК не работали ни для одного способа входа. Введена единая точка выдачи сессии (`AuthService::issue_session`), фронт полностью перешёл на `credentials: same-origin` без чтения токена из JS. | `auth/controller.rs`, `frontend/src/api.ts` | | 2 | **High** | **`localStorage` для сессионного токена** на лендинге (`netrunner-landing`) и в Tauri-приложении — ровно то, чего требовалось избежать. Лендинг переведён на ту же HttpOnly cookie-сессию через `credentials: "include"` и общий родительский домен (`Domain=.netrunner-vpn.com`); `netrunner-app` вне скоупа этого прохода (её `localStorage`-черновик отключён и не используется в рантайме). | `netrunner-landing/app/[lang]/auth/auth-client.tsx` | | 3 | **Medium** | **Отсутствие refresh-флоу**: access-JWT жил 10 минут без какого-либо обновления — юзера тихо разлогинивало каждые 10 минут. Мёртвая таблица `auth_sessions` (недописанный Magic Link) repurposed под `refresh_sessions`: ротируемый непрозрачный refresh-токен (30 дней, только SHA-256-хеш в БД), при обнаружении повторного использования уже отозванного токена — отзыв **всех** сессий юзера (сигнал компрометации). | `auth/service.rs`, `migrations/0002_refresh_sessions.sql` | | 4 | **Medium** | **Cyberhack: сервер доверял `score` от клиента** (клампился 0..=500, не более). Теперь доску генерирует и хранит сервер (`users/cyberhack.rs`, порт алгоритма 1:1 из `Cyberhack/src/lib.rs`), клиент присылает только сырой путь кликов, сервер сам реплеит его и валидирует легальность каждого хода; повторная сдача той же сессии отклоняется (`used_at`). | `users/cyberhack.rs`, `users/service.rs`, `migrations/0003_hack_sessions.sql` | | 5 | **Low** | **CSRF/CORS были захардкожены на один домен** (`https://netrunner-vpn.com`) — не пропускали легитимные cookie-запросы с сабдомена ЛК (`account.netrunner-vpn.com`). Вынесены в конфигурируемые списки (`CSRF_ALLOWED_ORIGINS`, `CORS_ALLOWED_ORIGINS`). | `auth/guard.rs`, `api.rs` | | 6 | **Low** | **Курс TON — TonAPI отдаёт валюты в ВЕРХНЕМ регистре** (`prices.USD`), код сверял по нижнему (`prices.usd`) и хардкодил запрос только `currencies=usd,rub` — курс для остальных валют из `plan_prices` (CNY/TRY/IRR) не находился никогда. Заодно добавлено кеширование в Redis (TTL 60с). | `billing/service.rs::initiate_payment` | ## Проход 2026-06-26 ## Исправлено в этом проходе | # | Severity | Что | Где | |---|----------|-----|-----| | 1 | **High** | **Sybil-абуз триала**: анонимные Ghost-аккаунты (вход по Seed) штампуются бесплатно и без лимита, каждый получал бесплатный 3-дневный триал ⇒ бесконечный бесплатный VPN скриптом. Теперь триал только для аккаунтов с привязкой к Telegram (`tg_id`). | `billing/service.rs::activate_trial` | | 2 | **Medium** | **Timing-атака на Telegram-аутентификацию**: HMAC-тег сравнивался обычным `!=` по hex-строке (выход на первом несовпавшем байте → утечка по таймингу). Заменено на постоянное по времени `mac.verify_slice`. | `auth/service.rs::verify_tg_widget_auth` | | 3 | **Medium** | **CSRF-модель**: проверка Origin/Referer применялась ко всем мутирующим запросам, включая Bearer-токен (для которого CSRF неактуален) — это и ломало приложение, и неверно по модели. Теперь CSRF проверяется только для cookie-сессий; Bearer-клиенты не блокируются. | `auth/guard.rs::auth_guard` | | 4 | **Low** | **Инвойс без проверки владения**: `confirm_payment` игнорировал юзера (`_user` + TODO), позволяя инициировать обращения к блокчейну по произвольным `invoice_id` (перебор/DoS). Добавлена проверка владельца. | `billing/service.rs`, `billing/controller.rs` | | 5 | **Low** | **Паники в провижининге нод**: `Session::new().unwrap()`, `channel_session().unwrap()`, `read_to_string().unwrap()`, `wait_close().unwrap()`, `GHCR_TOKEN.expect()` → корректные `AppError`. Отсутствие `init_node.sh` теперь жёсткая ошибка, а не деплой полуживой ноды через `echo`-заглушку. | `nodes/service.rs` | | 6 | **Low** | **Недописанная диагностика Cyberhack**: сервис маппил ошибку `"TOO_FREQUENT"`, которую репозиторий никогда не возвращал (и нет-билетов, и слишком-часто молча давали 0). Репозиторий теперь блокирует строку (`FOR UPDATE`) и возвращает точные причины (`NO_TICKETS` / `TOO_FREQUENT` / `NO_USER`). | `db/repository.rs`, `users/service.rs` | ## Проверено и признано безопасным - **SQL-инъекции** — нет: все запросы параметризованы (`bind`), динамической конкатенации SQL из пользовательского ввода не найдено. - **Гонки в экономике** — защищены БД-инвариантами: `referrals UNIQUE(referred_id)`, `promo_usages UNIQUE(promo_id, user_id)`, `invoices.external_id UNIQUE`, `subscriptions PK(user_id)`. Промокоды берут строку под `FOR UPDATE`. Двойное списание билета закрыто `FOR UPDATE`. - **Подмена цены/тарифа** — нет: `initiate_payment` берёт цену из `plan_prices` по серверу, сумму считает сам; клиент не влияет на сумму инвойса. - **Подтверждение оплаты** — `verify_payment` проверяет on-chain сумму (`>=`), адрес назначения и комментарий (`== invoice_id`); статус `paid` идемпотентен; один tx-хеш нельзя переиспользовать (UNIQUE `external_id`). - **Раскрытие ошибок** — наружу уходят общие сообщения, детали только в логи; секреты (JWT, bot token) не логируются. - **Self-referral** — заблокирован на этапе `process_login` (`referrer_tg != tg_id`). ## Остаточные риски (вынесено отдельно) - **`GET /nodes/routing` выдаёт `public_key`/`session_token` в контрактном виде**, которые ещё нужно консистентно проверять на стороне ноды (`netrunner-proxy`, отдельный репозиторий, использует ephemeral ECDH per-session, не статический публичный ключ) — само по себе выпускается корректно, но полная цепочка верификации на стороне прокси вне скоупа этого прохода. - **`netrunner-app` (Tauri)** по-прежнему содержит неиспользуемый `localStorage`-черновик Ghost Protocol в `src/lib/api.ts` — код мёртвый (`USE_STATIC_NODES=true`), но если его когда-нибудь включат как есть, он снова принесёт токен в `localStorage`. Вне скоупа этого прохода по решению заказчика. - **Health-check нод — только TCP-connect**, не полноценная проверка протокола (нода может слушать порт, но быть неработоспособной на уровне приложения). ## Гейты `cargo check` ✅ · `cargo clippy --all-targets -- -D warnings` ✅ · `cargo fmt --check` ✅ · `cargo test` (юнит + `tests/db_integration_test.rs` на живом Postgres) ✅, кроме одного предсуществующего несвязанного мисматча (`test_consume_ticket_and_reward`, заведена отдельная задача) · сквозная ручная проверка через `make dev` (auth/nodes/billing/hack) ✅.