52531dd41a
Auth was effectively broken for every login path: the Telegram handler built a Set-Cookie header but never attached it to the response, and Ghost Protocol (seed) login never set a cookie at all — so billing, the game, and admin actions never actually worked once the frontend was fixed to stop trying to read an HttpOnly cookie from JS. Replaced the bare 10-minute JWT with AuthService::issue_session: a 15-minute access cookie plus a rotating 30-day refresh cookie (opaque token, only its SHA-256 hash stored in the new refresh_sessions table); reusing an already- rotated refresh token now revokes every session for that user. CSRF/CORS origin allowlists moved from a single hardcoded domain to env-configured lists so the same session works across the landing and account subdomains. The bot's WebApp deep links (Личный Кабинет/Тарифы/Синдикат) now bridge through a short-lived bootstrap token exchanged via POST /auth/exchange instead of a bare access token in the URL. Nodes: /nodes/routing now serializes the tunnel_* fields, public_key, sni_domain and a one-time session token gated on an active subscription instead of a stub ip/port/protocol list; node provisioning returns 202 immediately and finishes in the background instead of blocking the request for the whole SSH run; a new background task TCP-pings nodes every 60s and flips online/offline itself; admin can now force-restart a node over SSH. Billing: TON exchange rate is cached in Redis (60s) instead of hitting TonAPI on every invoice, and the request/response now actually uses the requested currency and TonAPI's real (uppercase) key casing — previously only USD/RUB were ever requested and the lookup used the wrong case, so non-USD/RUB plans could never price correctly. Cyberhack: the server now generates and stores the board itself and replays the client's raw click path to compute the score, instead of clamping a client-reported number — closes the "final score is whatever the browser sends" hole flagged in the security audit. Frontend: api.ts no longer tries to read the HttpOnly session cookie from JS (that never worked) and instead relies on same-origin credentials plus a silent refresh-and-retry on 401; AdminDashboard's restart/delete actions are wired up; Auth.tsx drops the artificial delays and requires an explicit seed download/confirmation before continuing, since a lost Ghost seed is unrecoverable. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
10 KiB
10 KiB
Аудит безопасности — 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-хеш нельзя переиспользовать (UNIQUEexternal_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) ✅.