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 (обратная разработка), социальные векторы.