Files
tech-service-portal/docs/SECURITY-REVIEW.md
T

9.5 KiB

Ревью безопасности (08.10.2026)

Объём: backend (app/), веб-интерфейс и PWA (app/static/), Android (манифест, хранение), Docker/compose. Методы: чтение кода, автотесты (tests/test_security.py, 30+ проверок), bandit (статический анализ), pip-audit (уязвимости зависимостей). Запуск тестов: pytest tests (оставшиеся известные недостатки помечены xfail — когда их исправят, тест сообщит XPASS, и пометку нужно снять).

Итог

(Обновлено после исправлений: см. таблицы «Исправлено» и «Открытые».)

Базовая защита сделана правильно: пароли в bcrypt, короткий access-токен и refresh с ротацией и отзывом при повторе, алгоритм JWT зафиксирован, запросы только через ORM, права проверяются на сервере, чужие наряды и фото недоступны, интерфейс не использует innerHTML. Критичных уязвимостей, позволяющих получить доступ без пароля, не найдено. Найденные дефекты средней и высокой важности исправлены (см. ниже), остались замечания низкой важности.

Исправлено

№ Важность Что было Как исправлено
SEC-01 Средняя Ответ 1e999999, inf, nan в числовом пункте давал HTTP 500 app/logic.py: проверка math.isfinite, ответ 422
SEC-08 Высокая Пустой/дефолтный SECRET_KEY позволял подделывать токены (пустая переменная в compose даёт пустой ключ); пароль админа admin APP_ENV=production (в compose): отказ запуска при ключе короче 32 символов и слабом ADMIN_PASSWORD при первом создании; в разработке — предупреждение
SEC-10 Средняя --forwarded-allow-ips "*" — подделка X-Forwarded-For обходила лимит; блокировка по одному логину позволяла заблокировать вход любому Доверенные прокси берутся из FORWARDED_ALLOW_IPS (по умолчанию 127.0.0.1); лимиты: пара «адрес+логин» 5, весь адрес 30, логин целиком 20 за 5 минут; адрес клиента виден админу в настройках. Требует действий в инфраструктуре (адрес NPM, файрвол)
SEC-02 Средняя Пароль от 4 символов; длиннее 72 байт молча обрезался От 10 символов, не более 72 байт (создание, смена, сброс)
SEC-04 Средняя SSRF через адрес push-подписки Разрешены только https:// и домены push-сервисов (FCM, Mozilla, Apple, Windows), без IP, портов и учётных данных в адресе; старые подписки с другими адресами удаляются при рассылке; нужны ключи подписки (раньше — 500)
SEC-11 Средняя Android разрешал открытый HTTP network_security_config: в релизе только HTTPS, в debug — только эмулятор/localhost; экран входа отклоняет http://. Версия 1.0.1 (versionCode 2)
SEC-06 Низкая CSV-экспорт отчётов не защищал от формул (=, +, -, @) Значения экранируются апострофом (safe_cell), включая новый отчёт «Текущее состояние»
SEC-09 Низкая Словарь неудачных входов рос без предела Предел 5000 записей и очистка истёкших
SEC-13 (часть) Низкая Тайминг-перечисление логинов Для несуществующего логина выполняется фиктивная проверка bcrypt
SEC-14 (часть) Информационная PyJWT 2.9 с известными уязвимостями Обновлён до 2.15

Открытые замечания (рекомендуется исправить)

№ Важность Замечание Рекомендация
SEC-05 Низкая–средняя Нет заголовков безопасности: X-Content-Type-Options, X-Frame-Options/frame-ancestors, CSP, Referrer-Policy. Токены лежат в localStorage, поэтому CSP важна как вторая линия защиты. Middleware с заголовками; на NPM включить HSTS. CSP: default-src 'self'; img-src 'self' blob: data:; frame-ancestors 'none'.
SEC-07 Низкая /docs и /openapi.json открыты без авторизации — раскрывают всю поверхность API. Закрыть на NPM или отключить в боевом режиме (docs_url=None).
SEC-12 Низкая Образ запускается от root. USER app в Dockerfile (тома photos/appdata должны быть доступны на запись).
SEC-13 Низкая Access-токены других сессий действуют до 60 минут после смены/сброса пароля (refresh-токены отзываются сразу). Поле token_version в пользователе.
SEC-14 Информационная pip-audit: Starlette 0.46.2 (исправлено в 0.47–1.3) тянется через fastapi==0.115. Обновить FastAPI/Starlette отдельным шагом с прогоном тестов; pip-audit включить в CI.

Что проверено и работает (тесты)

  • Аутентификация: все маршруты /api/v1/*, кроме login/refresh/logout/health/demo-status, дают 401 без токена (проверяется перебором всех маршрутов приложения); поддельные токены (чужой ключ, alg=none, просроченный, токен демо-режима в боевом, несуществующий пользователь, мусор) отклоняются; refresh-токен не работает как access.
  • Сессии: ротация refresh, отзыв всех сессий при повторном предъявлении старого токена, выход, смена и сброс пароля завершают прочие сессии, отключённый пользователь теряет доступ сразу (access, refresh, вход).
  • Перебор: блокировка после 5 неудач, одинаковый ответ для неизвестного и неверного пароля.
  • Права: техник не имеет доступа к управлению, настройкам, отчётам, демо; инженер не управляет пользователями и настройками; модератор не может выдать права, менять администраторов и модераторов, сбросить пароль админа; нельзя удалить себя.
  • Изоляция данных: техник не видит и не может менять чужие наряды, комментарии, фото; item_id из другого наряда не принимается; исполнитель не может проверить свой наряд; после сдачи ответы править нельзя; завершённый наряд не отменить.
  • Файлы: HTML/SVG/exe/PHP под видом image/png отклоняются (415), лимит размера (413), имена на диске создаёт сервер, обход каталогов через /static невозможен.
  • Инъекции: SQL-инъекции во входе, поиске и фильтрах не работают, %/_/\ в поиске экранируются; XSS-вставки хранятся как текст и отдаются как JSON; во фронтенде нет innerHTML/eval (тест сканирует исходники).
  • Статический анализ bandit: 4 замечания низкой важности (демо-пароль demo1234, random для картинок демо, пустой except) — не влияют на безопасность.

Не проверялось

Нагрузочные и сетевые атаки (DoS на уровне прокси), настройки NPM/TLS и файрвола Coolify, безопасность хоста и резервных копий, реальный Firebase-ключ, пентест подписанного APK (обратная разработка), социальные векторы.