Пен тест #25
Notifications
Due Date
No due date set.
Depends on
#24 Добавление аутентификации логин/пароль
NotBigGhost/ForbiddenStarsApp
Reference: NotBigGhost/ForbiddenStarsApp#25
Reference in New Issue
Block a user
Упор на аутентификации и проходу по апи, дальше остальное.
/secure-review
Зависит от #24
План выполнения
Пен-тест контура аутентификации и прохода по API после #24 (вход по логину/паролю стал основным
во всех окружениях). Задача исследовательская: код в этом проходе не меняется, результат —
отчёт и заведённые по находкам задачи. Ветка и PR не создаются.
(отзыв при logout / смене пароля), Telegram HMAC, CSRF double-submit, атрибуты cookie.
get_current_user/get_current_adminна каждомэндпойнте, IDOR, доверие к
X-Forwarded-For(--forwarded-allow-ips=*).SECRET_KEY/ADMIN_PASSWORD, открытость/api/docs, in-memorythrottle, заголовки безопасности, валидация ввода.
throttle игрока, перебор пароля админа, выживание JWT после logout/смены пароля, CSRF, IDOR,
подмена
X-Forwarded-For, открытость/api/docs.forbidden-stars.ru(когда test-клон поднят): заголовки, TLS,/api/docs. Перебор и мутации не выполняются. Прод не трогаем.Критерии готовности
Kind/Security) со ссылкой на #25.Ветка: не создаётся (исследовательская задача, кода нет).
Пен-тест #25 — отчёт
Проверка безопасности контура аутентификации и прохода по API после #24 (вход по
логину/паролю стал основным во всех окружениях). Упор — на аутентификацию, как задано в теле.
Находки уровня High поданы как класс проблемы + условия, без готового пошагового эксплойта.
Код в этом проходе не менялся — на каждую находку заведена отдельная задача.
Резюме
X-Forwarded-ForSECRET_KEY/ADMIN_PASSWORD/api/auth/registerбез throttle + перечисление никовЧто проверено
Метод: статический разбор кода аутентификации/авторизации + динамические проверки на
изолированном экземпляре приложения (in-memory БД, данные dev/prod не затрагивались),
плюс неразрушающая проверка edge (TLS/заголовки) на test-домене. Мутаций и перебора на живых
контурах не выполнялось. Базовый прогон:
pytest— 131 passed.Область и вердикты:
6-й попытке), единая ошибка
INVALID_CREDENTIALSи для неизвестного логина, и для неверногопароля (нет user-enumeration), фиктивный хеш выравнивает тайминг, вход только
role='player'.X-CSRF-Token→ 403CSRF_FAILED,с корректным токеном → 200; токен перевыдаётся при потере cookie.
SHA256(bot_token),hmac.compare_digest,проверка свежести
auth_date(сутки). Привязка требует авторизации и CSRF, занятый Telegram → 409.httponlyна сессионных,secureвыводится из окружения,samesite=lax,путь админ-cookie ограничен
/api/admin; CSRF-cookiehttponly=False(нужно для double-submit).assert_member/assert_can_modify.в 422 без сырых байт (уже чинилось в #52).
resolve()+ проверка_STATIC_DIR in parents).localStorageтокенов нет; CSRF из cookie.Модель угроз
fs_session7 дней,fs_admin8 ч), партии/статистика, аудит-журнал.украденного/утёкшего токена, (г) сетевой наблюдатель. Точка входа одна — публичный HTTPS через
Caddy на VPS → SSH-туннель → приложение (uvicorn 1 воркер).
приложение доверяет
X-Forwarded-*— см. F3), приложение↔SQLite.бессрочность украденного токена (F2); снятие лимита перебора и порча аудита подставным IP (F3);
компрометация через дефолтные секреты при небрежном деплое (F4).
поставки образов, соц.инженерия, безопасность самого Telegram-аккаунта игрока.
Находки
F1 — Вход администратора без защиты от перебора (High)
Где:
routers/admin.py:36→services/admin_service.py:authenticate_admin.Суть: вход игрока обёрнут в
login_throttle(auth/password.py), а вход админа — нет.Пароль администратора — единственный барьер к полному контролю приложения, и его можно
перебирать онлайн без ограничения частоты.
Условия/evidence: 25 последовательных попыток входа с неверным паролем — все
401, ни одного429. Логина-обёртки с лимитом на этом пути нет.Рекомендация: пропустить
authenticate_adminчерез тот жеlogin_throttle(ключи по IP и поusername), с отдельным, более строгим лимитом; фиксировать серию неудач в аудите.F2 — JWT не отзывается при выходе и смене пароля (High)
Где:
routers/auth.py:118(logout),core/security.py,auth/deps.py,routers/users.py:121(смена пароля),routers/admin.py:115(сброс админом).Суть:
logoutлишь просит браузер удалить cookie; серверного списка отозванных токенов иливерсии токена нет —
decode_tokenпроверяет только подпись/aud/exp. Украденный или оставшийсятокен остаётся валидным до истечения TTL (игрок — 7 дней), в том числе после выхода и
после смены пароля. Сценарий сброса «забытого» пароля админом это тоже не закрывает: если
аккаунт был скомпрометирован, старые сессии злоумышленника продолжают работать.
Условия/evidence: старый токен, предъявленный вручную после
logout→GET /api/users/me=200;то же после смены пароля →
200.Рекомендация: ввести
token_versionу пользователя (claim в JWT, сверка вdeps), инкрементпри смене пароля и при «выйти со всех устройств»; либо denylist по
jti/ отсечение поiat < password_changed_at.F3 — Обход throttle и подмена IP через
X-Forwarded-For(High; подтвердить на живом контуре)Где:
entrypoint.sh(--forwarded-allow-ips="*"),core/security.py:client_ip,core/ratelimit.py,deploy/vps/Caddyfile.Суть: uvicorn запущен с
--forwarded-allow-ips="*"→ доверяет заголовкуX-Forwarded-Forотлюбого пира и берёт из него левое (клиентское) значение
(
ProxyHeadersMiddleware.get_trusted_client_address). Caddy добавляет реальный IP справа, поэтомуподставленное клиентом левое значение сохраняется и становится
request.client.host. Оба ключаthrottle (
login:{ip}:{nickname}иlogin-ip:{ip}) содержат этот IP — значит ротацияX-Forwarded-Forна каждый запрос полностью снимает лимит перебора пароля. Тот же IP пишется ваудит (
audit_service) — журнал источника подделывается (анти-форензика).Условия/evidence: подтверждено чтением кода uvicorn (левое значение при
*) и Caddyfile(append XFF). Полную эксплуатацию на живом контуре в рамках задачи не выполняли (без перебора на
проде/тесте) — рекомендуется подтвердить на test-клоне.
Рекомендация: доверять
X-Forwarded-Forтолько фактическому прокси (loopback / адрес туннеля),а на Caddy перезаписывать заголовок реальным пиром (
header_up X-Forwarded-For {remote_host})вместо append; тогда
client_ipснова достоверен и для throttle, и для аудита.F4 — Нет fail-fast на дефолтных
SECRET_KEY/ADMIN_PASSWORD(High, misconfiguration)Где:
core/config.py(дефолтыsecret_key="change-me-dev-secret-not-for-production",admin_password="change-me-admin-password"),bootstrap.py:_ensure_admin.Суть: приложение стартует в
productionс дефолтными секретами без предупреждения.bootstrapотклоняет лишь пустой пароль админа, но не дефолтный. Деплой, скопировавший
.env.exampleдословно, получает: известный всем ключ подписи JWT (→ подделка любого токена, включая админский)
и известный пароль администратора.
(Секреты прода в рамках задачи не читались — проверяется само отсутствие защиты от дефолта.)
Рекомендация: при
is_productionпадать на старте, еслиSECRET_KEY/ADMIN_PASSWORDравныдефолтам или слишком коротки; вынести проверку в
config/bootstrap.F5 — In-memory throttle: сброс при рестарте и неэффективность при масштабировании (Medium)
Где:
core/ratelimit.py.Суть: счётчики живут в памяти процесса. Любой рестарт (в т.ч. деплой) обнуляет окно 15 минут;
при уходе от
--workers 1лимит делится между воркерами. В сумме с F3 (подставной IP) онлайн-перебор пароля практически не ограничен.
Рекомендация: при переходе на несколько воркеров — вынести счётчики во внешний стор (Redis);
добавить глобальный лимит на аккаунт, не зависящий от IP.
F6 — Раскрытие: открытая схема API + отсутствие security-заголовков (Low)
Где:
main.py:157(openapi_url/docs_url/redoc_url),deploy/vps/Caddyfile.Суть:
/api/openapi.json(68 путей),/api/docs,/api/redocдоступны без авторизации во всехокружениях — облегчает разведку поверхности API. На edge (Caddy) не выставляются HSTS, CSP,
X-Content-Type-Options,X-Frame-Options,Referrer-Policy,Permissions-Policy; в ответе виденServer: Caddy.Условия/evidence: локально
GET /api/openapi.json=200(68 путей); на test-домене в ответетолько
Cache-Control: no-storeиServer: Caddy, security-заголовков нет (TLS-сертификат валиден).Рекомендация: закрыть docs/openapi в
production(или под админ-сессией); добавить security-заголовки в общий сниппет
(edge)Caddyfile; убрать/переопределитьServer.F7 —
/api/auth/registerбез throttle + перечисление ников (Low)Где:
routers/auth.py:34,routers/users.py:258(nickname-available),:268(search).Суть: регистрация не ограничена по частоте (спам аккаунтов); существующий ник →
409 NICKNAME_TAKEN, что даёт перечисление занятых ников. Авторизованныеnickname-availableиsearchпозволяют то же вошедшему. Вход (login) даёт единую ошибку — там enumeration нет.
Рекомендация: throttle на
register(по IP); перечисление ников через занятость — принять какмалый риск (ники и так публичны в топе) либо смягчить.
Q1 — Профиль и история любого игрока доступны любому вошедшему (Info, требует решения)
Где:
routers/users.py:211(/{id}/profile),:220(/{id}/matches).Суть: эндпойнты защищены только
get_current_user— вошедший может перебором по id получитьпрофиль и историю партий любого игрока (историю ограничивает «витрина владельца» all/best). Аноним
получает
401— это норма. Вероятно, задумано (соц.функция сайта), но фиксирую для явного решениявладельца, стоит ли ограничивать.
Статус:
Status/In Review— жду вашего утверждения отчёта и списка задач. После утверждениязадачи готовы к приоритизации (
Priority/*расставляете вы).Заведённые задачи по находкам (веха «1.37», метка
Kind/Security, приоритеты не проставлял — за вами):X-Forwarded-Forснимает throttle и портит аудит (High)SECRET_KEY/ADMIN_PASSWORD(High)Q1 (профиль/история любого игрока доступны любому вошедшему) отдельной задачей не заводил —
похоже на осознанный дизайн соц.функции. Скажите, ограничивать ли, и тогда заведу.
Осталось за вами: утвердить отчёт и приоритизировать задачи. Живую эксплуатацию F3 (перебор на
test-клоне) по вашему слову могу подтвердить неразрушающе, когда
forbidden-stars.ruбудет поднят.