supersam/docs/security-audit-remediation-...

404 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# План устранения уязвимостей и защиты персональных данных
Дата аудита: 2026-04-29
## 1. Контекст и поверхность атаки
Приложение работает как веб-кабинет доставки с персональными данными клиентов: ФИО, телефон, адрес, состав заказа, история доставки, выбранные слоты, сообщения и статусы. Основные поверхности атаки:
- `/login` — вход сотрудников по email OTP.
- `/dashboard` — авторизованные кабинеты сотрудников.
- `/delivery/:token` — публичная клиентская ссылка без логина.
- Supabase Edge Functions — публичные и интеграционные endpoints.
- Supabase Postgres — таблицы `orders`, `users`, `delivery_invitations`, `chat_messages`, `order_history`, `delivery_slots`, `integration_events`.
- PWA/localStorage — локальное хранение состояния и клиентских приглашений.
## 2. Что уже сделано хорошо
- Включены RLS-политики на основных таблицах: `roles`, `users`, `orders`, `order_history`, `delivery_slots`, `chat_messages`, `delivery_invitations`, `integration_events`.
- Для `orders` доступ ограничен ролью и назначением: менеджер, логист, водитель, администратор.
- Invitation token в БД хранится как `token_hash`, а не как открытый token.
- Клиентская ссылка работает без авторизованного пользователя, но доступ идёт по token.
- Email OTP настроен с `shouldCreateUser: false`, то есть саморегистрация через фронтенд не должна создавать новых пользователей.
## 3. Критические риски
### P0. Нет rate limits на OTP, публичные ссылки и Edge Functions
Найдено:
- `src/context/AuthContext.jsx:117` вызывает `supabase.auth.signInWithOtp(...)` напрямую из браузера.
- `supabase/functions/get-delivery-invitation/index.ts:57` и `supabase/functions/confirm-delivery-choice/index.ts:14` принимают публичные запросы без лимитов.
- `supabase/functions/create-delivery-invitation/index.ts:17`, `transfer-to-logistics`, `report-delivery-result`, `chatbot-webhook` также не имеют throttle/rate limit.
Риск:
- Заспамить OTP-письма на один email или набор email.
- Брутфорсить invitation token.
- Массово дергать клиентские ссылки и собирать признаки существующих заказов.
- Перегрузить Edge Functions и базу.
- Создать много `delivery_slots`, `order_history`, `integration_events` через повторные запросы.
План устранения:
1. Добавить таблицу `public.rate_limits`:
```sql
create table public.rate_limits (
id uuid primary key default gen_random_uuid(),
scope text not null,
key text not null,
window_start timestamptz not null,
count integer not null default 1,
blocked_until timestamptz,
created_at timestamptz not null default timezone('utc', now()),
updated_at timestamptz not null default timezone('utc', now()),
unique (scope, key, window_start)
);
```
2. Добавить SQL/RPC `public.check_rate_limit(scope, key, max_count, window_seconds, block_seconds)` с атомарным `insert ... on conflict ... update`.
3. Для Edge Functions сделать shared helper `supabase/functions/_shared/rate-limit.ts`.
4. Лимиты по умолчанию:
| Endpoint | Ключ | Лимит |
|---|---|---|
| OTP request | `ip + email_hash` | 3 запроса / 10 минут, блок 30 минут |
| OTP verify | `ip + email_hash` | 5 попыток / 10 минут, блок 30 минут |
| get invitation | `ip + token_hash_prefix` | 30 запросов / 10 минут |
| confirm delivery | `ip + token_hash` | 5 POST / 10 минут, блок 1 час |
| create invitation | `integration_key + order_id` | 10 запросов / 10 минут |
| chatbot webhook | `provider + external_message_id/ip` | 60 запросов / минуту |
| report delivery result | `integration_key + order_id` | 10 запросов / 10 минут |
5. Для `/login` лучше не вызывать Supabase OTP напрямую из браузера, а добавить Edge Function `request-otp`, которая валидирует email, применяет rate limit и только потом инициирует OTP.
6. Добавить тесты на rate-limit helper: allowed, exhausted, blocked, reset after window.
### P0. Service role используется в публичных functions без отдельной авторизации
Найдено:
- `supabase/functions/_shared/chatbot.ts:18` создаёт Supabase client через `SUPABASE_SERVICE_ROLE_KEY`.
- `create-delivery-invitation`, `confirm-delivery-choice`, `transfer-to-logistics`, `report-delivery-result`, `chatbot-webhook` используют service client и обходят RLS.
- `create-delivery-invitation` принимает `orderId` из тела запроса и обновляет заказ через service role.
- `report-delivery-result` и `transfer-to-logistics` также меняют заказ по `orderId` без проверки пользователя/подписи.
Риск:
- Если endpoint доступен извне, злоумышленник может менять статусы заказов, создавать приглашения, переводить в платное хранение или доставлено, если узнает/подберёт `orderId`.
- RLS не помогает, потому что service role её обходит.
План устранения:
1. Разделить Edge Functions на публичные и интеграционные:
- публичные: `get-delivery-invitation`, `confirm-delivery-choice`;
- внутренние/integration-only: `create-delivery-invitation`, `transfer-to-logistics`, `report-delivery-result`, `chatbot-webhook`, `send-chatbot-message`.
2. Для internal functions требовать `Authorization: Bearer <INTEGRATION_API_KEY>` или HMAC-подпись:
- `X-Signature: hex(hmac_sha256(raw_body, secret))`
- `X-Timestamp`
- `X-Request-Id`
3. Отклонять запросы старше 5 минут.
4. Хранить обработанные `X-Request-Id` в `integration_events` или отдельной таблице `idempotency_keys`.
5. Добавить `verifyIntegrationRequest(request, rawBody)` в `_shared/auth.ts`.
6. Для functions, вызываемых из авторизованного UI, проверять JWT пользователя через anon client или `supabase.auth.getUser(jwt)`, а service role использовать только после проверки роли.
### P0. CORS открыт на все origin
Найдено:
- `Access-Control-Allow-Origin: "*"` в `get-delivery-invitation`, `confirm-delivery-choice`, `create-delivery-invitation`, `transfer-to-logistics`, `report-delivery-result`, `chatbot-webhook`.
Риск:
- Любой сайт может дергать публичные endpoints из браузера.
- Для публичной клиентской ссылки это допустимо только частично, но для integration-only endpoints опасно.
План устранения:
1. Добавить shared helper `cors.ts`.
2. Настроить allowlist через env:
- `PUBLIC_APP_URL`
- `APP_ALLOWED_ORIGINS`
- `INTEGRATION_ALLOWED_ORIGINS`
3. Для public invitation endpoints разрешить только домены приложения.
4. Для integration-only endpoints либо не отвечать CORS вообще, либо разрешить только внутренний origin/n8n.
5. На неизвестный origin возвращать `403`.
### P1. Token публичной ссылки не имеет срока жизни и политики ротации
Найдено:
- `delivery_invitations` не содержит `expires_at`.
- `get-delivery-invitation` возвращает данные, если token найден и состояние вычислено по заказу.
- `confirm-delivery-choice` проверяет active state, но не проверяет срок жизни token.
Риск:
- Старая ссылка остаётся рабочей слишком долго.
- Утечка ссылки даёт доступ к данным заказа и действию подтверждения.
План устранения:
1. Добавить поля:
```sql
alter table public.delivery_invitations
add column if not exists expires_at timestamptz,
add column if not exists revoked_at timestamptz,
add column if not exists last_accessed_at timestamptz,
add column if not exists access_count integer not null default 0;
```
2. При создании invitation ставить `expires_at = now() + interval '7 days'` или другой срок по ТЗ.
3. В `get-delivery-invitation` и `confirm-delivery-choice` возвращать `410 Gone`, если `expires_at < now()` или `revoked_at is not null`.
4. После успешного `confirm-delivery-choice` запрещать повторное подтверждение, кроме идемпотентного возврата текущего выбора.
5. Добавить ротацию token при повторной отправке ссылки.
### P1. Публичная ссылка раскрывает лишние персональные данные
Найдено:
- `get-delivery-invitation` возвращает `customerName`, `customerPhone`, `orderNumber`, `orderItems`, `orderStatus`, `deliveryAgreementStatus`.
Риск:
- При утечке token третье лицо видит телефон, имя, состав заказа и статус.
- Эти данные могут быть использованы для социальной инженерии.
План устранения:
1. Минимизировать ответ публичной страницы:
- показывать имя частично: `Мария В.`;
- телефон маскировать: `+7 *** ***-12-31`;
- номер заказа показывать частично или только последние 4 символа;
- состав заказа показывать укрупнённо, если это не нужно клиенту для выбора слота.
2. Не возвращать `orderStatus` и `deliveryAgreementStatus` в сыром виде, отдавать только public state: `awaiting_choice`, `agreed`, `delivered`, `expired`.
3. В `delivery_invitations` хранить public-safe snapshot, чтобы не тянуть весь `orders.customer`.
4. Добавить тест, что public API не отдаёт полный телефон и внутренний статус.
### P1. Webhook не проверяет подпись провайдера и не ограничивает provider
Найдено:
- `chatbot-webhook` берёт `provider` из query string.
- Тело события нормализуется без проверки подписи.
- `external_message_id` есть, но повторная вставка при конфликте может давать ошибку и 500, а не идемпотентный `ok`.
Риск:
- Любой может отправить fake webhook и изменить заказ.
- Возможен replay одного webhook.
- Возможна подмена provider.
План устранения:
1. Для каждого provider добавить проверку подписи:
- Telegram: secret token/header или webhook secret path.
- VK/Max: HMAC/secret по контракту провайдера.
2. Убрать произвольный `provider` из query или валидировать строго по allowlist.
3. Для `external_message_id` реализовать `upsert`/идемпотентный возврат `ok: true` при повторе.
4. В payload логировать только безопасные поля, без полного сырого тела, если оно содержит ПДн.
### P1. Нет ограничений на размер JSON и длину полей
Найдено:
- Edge Functions делают `await request.json()` без проверки `Content-Length`.
- `chatbot-webhook` записывает `text` и `payload` как пришли.
- `confirm-delivery-choice` принимает `deliveryDate` и `deliveryTime` без строгой проверки формата и allowed values.
Риск:
- DoS через большой body.
- Загрязнение истории/логов огромным payload.
- Некорректные даты и значения слотов.
План устранения:
1. Добавить helper `readJsonBody(request, { maxBytes })`.
2. Лимиты:
- public endpoints: 8 KB;
- webhook: 64 KB;
- internal dispatch: 16 KB.
3. Валидировать:
- UUID для `orderId`;
- `deliveryDate` как `YYYY-MM-DD`;
- `deliveryTime` только из `available_slots`;
- `note`, `reason`, `text` по максимальной длине.
4. Возвращать `413 Payload Too Large` и `422 Unprocessable Entity`.
### P1. Политики RLS слишком широкие для некоторых ролей
Найдено:
- `users self or admin` фактически разрешает всем авторизованным пользователям читать всех пользователей.
- `orders update by workflow role` позволяет `production_lead` обновлять все заказы.
- `orders insert managers admin` всё ещё разрешает менеджеру создавать заказы, хотя по текущему ТЗ заказы приходят из 1С.
Риск:
- Лишний доступ к email/пользователям.
- Шире нужного доступ к заказам и ПДн.
- Внутренний пользователь может создать заказ вручную.
План устранения:
1. Разделить policy users:
- self может читать только себя;
- admin читает всех;
- при необходимости логист/менеджер получают только `id, name, role`, без email/last_login.
2. Запретить insert заказов из UI для manager, если 1С является единственным источником.
3. Ограничить `production_lead` только source/production полями или убрать роль из текущего демонстрационного контура.
4. Добавить database policy tests через Supabase local или SQL assertions.
### P2. Локальный fallback содержит демонстрационные персональные данные
Найдено:
- `src/services/deliveryInvitationApi.js` содержит `showcase`, `client-flow-*`, имя, телефон и состав заказа.
- `AuthContext` в fallback принимает OTP `000000`.
Риск:
- При production build без env приложение может работать в локальном/demo режиме.
- Демонстрационные данные могут быть восприняты как реальные.
План устранения:
1. Добавить build guard: production build падает без `VITE_SUPABASE_URL` и `VITE_SUPABASE_ANON_KEY`.
2. Обернуть fallback только в `import.meta.env.DEV`.
3. В production не поддерживать `showcase` и `client-flow-*`.
4. Добавить тест, что в production mode fallback недоступен.
### P2. Ошибки возвращают внутренние сообщения наружу
Найдено:
- Edge Functions возвращают `error instanceof Error ? error.message : "Unexpected error"` клиенту.
Риск:
- Утечка деталей Supabase, структуры таблиц, provider errors.
План устранения:
1. Ввести `publicError(status, code, message)` и `logInternalError(error, context)`.
2. Клиенту отдавать стабильные коды: `not_found`, `expired`, `rate_limited`, `invalid_payload`, `temporary_error`.
3. Внутреннюю ошибку писать в `error_logs`/observability без ПДн.
## 4. План работ по этапам
### Этап 1. Срочная защита от спама и неавторизованных изменений
Срок: 1-2 дня.
- Добавить `rate_limits` и shared rate-limit helper.
- Закрыть `create-delivery-invitation`, `transfer-to-logistics`, `report-delivery-result`, `send-chatbot-message`, `chatbot-webhook` через integration secret/HMAC.
- Ограничить CORS для Edge Functions.
- Добавить проверку размера JSON body.
- Добавить smoke-тесты на `429`, `401`, `403`.
Критерий готовности:
- Нельзя массово дергать OTP/public endpoints без `429`.
- Нельзя изменить заказ через integration endpoint без валидного секрета.
- Unknown origin не получает CORS-доступ.
### Этап 2. Защита публичной клиентской ссылки
Срок: 1-2 дня.
- Добавить `expires_at`, `revoked_at`, `access_count`, `last_accessed_at`.
- Проверять срок действия token.
- Маскировать phone/name/orderNumber.
- Убрать внутренние `orderStatus` и `deliveryAgreementStatus` из public response.
- Проверять `deliveryDate/deliveryTime` против доступных слотов.
- Сделать подтверждение выбора идемпотентным.
Критерий готовности:
- Просроченная ссылка возвращает `410`.
- Public API не отдаёт полный телефон и внутренние статусы.
- Повторное подтверждение не создаёт дубли `delivery_slots`.
### Этап 3. Ужесточение RLS и ролей
Срок: 1 день.
- Сузить `users` policy.
- Запретить создание заказов менеджером, если источник только 1С.
- Проверить права `production_lead` и убрать из текущего UI/политик, если роль не используется.
- Добавить SQL tests на доступ manager/logistician/driver/admin.
Критерий готовности:
- Сотрудник не может читать список email всех пользователей без нужной роли.
- Менеджер не создаёт заказ напрямую через Supabase client.
- Водитель видит только свои доставки.
### Этап 4. Production hardening
Срок: 1 день.
- Отключить demo/local fallback в production.
- Добавить security headers:
- `Content-Security-Policy`;
- `X-Content-Type-Options: nosniff`;
- `Referrer-Policy: no-referrer`;
- `Permissions-Policy`;
- `Strict-Transport-Security` на уровне хостинга.
- Убедиться, что service worker не кеширует ответы с ПДн.
- Добавить redaction для логов: телефоны, email, адреса, token.
Критерий готовности:
- Production build невозможен без Supabase env.
- В кэше нет API-ответов с ПДн.
- Логи не содержат полные телефоны/email/token.
## 5. Рекомендуемые значения лимитов
| Сценарий | Лимит | Блокировка |
|---|---:|---:|
| Запрос OTP на email | 3 / 10 минут | 30 минут |
| Проверка OTP | 5 / 10 минут | 30 минут |
| Открытие public invitation | 30 / 10 минут | 10 минут |
| Подтверждение выбора доставки | 5 / 10 минут | 1 час |
| Создание invitation | 10 / 10 минут на orderId | 30 минут |
| Webhook сообщения | 60 / минуту на provider | 5 минут |
| Ошибки 4xx/5xx с одного IP | 100 / 10 минут | 30 минут |
## 6. Проверки после внедрения
- Unit tests для rate-limit helper.
- Edge Function tests:
- no secret -> `401`;
- wrong origin -> `403`;
- too many requests -> `429`;
- too large body -> `413`;
- invalid payload -> `422`;
- expired invitation -> `410`.
- RLS tests:
- manager видит только свои заказы;
- logistician видит только назначенные наборы;
- driver видит только назначенные доставки;
- обычный пользователь не читает всех `users`.
- Manual security checklist:
- OTP нельзя заспамить;
- публичная ссылка не раскрывает полный телефон;
- повторный webhook не дублирует событие;
- интеграционный endpoint без подписи не меняет заказ.
## 7. Приоритетный backlog
1. `P0` Rate limits для OTP и public/integration Edge Functions.
2. `P0` Integration auth/HMAC для service-role endpoints.
3. `P0` CORS allowlist вместо `*`.
4. `P1` Expiration/revocation invitation token.
5. `P1` Минимизация ПДн в public invitation response.
6. `P1` Валидация payload и body size limits.
7. `P1` Ужесточение RLS для `users` и `orders insert`.
8. `P2` Production build guard против demo fallback.
9. `P2` Security headers и service-worker cache policy.
10. `P2` Redaction логов и мониторинг подозрительной активности.