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