# Доменные блокеры F-T1/F-T2/F-P1 — верификация (16.06.2026)

> Тип: audit-findings (положено в plans/ т.к. свободная запись стены — только specs/plans).
> Контекст: доработка Этапа 2б доменного аудита.

Три ранее найденных блокера проверены статически по коду (local `main` == prod после merge
lead-region, прод-паритет app-кода подтверждён хешами) + live-чтением прод-конфигурации через
бастион (`ssh liderra-prod`). Объект: боевой `liderra-test` / локальный `main` (HEAD `c4cb766d`).

---

## F-T2 — `/api/admin/*` без app-гейта → 🟡 ПРИКРЫТ НА КРАЮ (хрупко)

**Код:** `app/app/Http/Middleware/EnsureSaasAdmin.php` — `handle()` = чистый pass-through
(`return $next($request)`). App-уровень админ-зону НЕ закрывает. `admin_user_id` для audit-trail
резолвится трейтом `ResolvesAdminUserId` как стаб `super_admin`.

**Роуты:** `app/routes/web.php:86` — `Route::middleware('saas-admin')->group(...)` оборачивает
весь `/api/admin/*`: impersonation (вход под тенанта), правка баланса, refund/смена тарифа,
system-settings, pricing-tiers, supplier prices, **152-ФЗ erasure** (`/api/admin/pd-subject-requests/{id}/erase`).

**Защита (проверено на проде):** `/etc/nginx/sites-enabled/liderra`:

```
location ^~ /api/admin {
    auth_basic "Liderra Admin";
    auth_basic_user_file /etc/nginx/.htpasswd-admin;
}
```

Также `location ^~ /admin` (стр. 56-57) под тем же Basic Auth. Снаружи без пароля зона недоступна.

**Вердикт:** не открытая дыра — краевая HTTP Basic Auth активна. **Остаточные риски (захардить):**

1. Один общий пароль (`.htpasswd-admin`) — нет пер-админ идентичности.
2. Аудит-атрибуция фиктивна: `admin_user_id` = стаб `super_admin` → кто выполнил impersonation /
   refund / erasure — в `saas_admin_audit_log` не различимо.
3. App-гейт = no-op → единственный слой защиты (если nginx-конфиг будет изменён/обойдён —
   зона оголяется).

**Правильный фикс:** настоящий saas-admin guard (Yandex 360 SSO-сессия + роль), вернуть проверку
в `EnsureSaasAdmin`. **Блокирован Б-1 + DO-4** (открытые вопросы — не закрывать без заказчика).

---

## F-P1 — 152-ФЗ erasure: `deals.phones[]` не чистится → 🔴 ОТКРЫТ (единственный actionable)

**Код:** `app/app/Services/Pd/PdErasureService.php::eraseSubject()`.

**Дефект 1 — JSONB не анонимизируется.** Схема `db/schema.sql:1633`: `deals.phones JSONB
-- дополнительные телефоны`. Сервис у `deals` обновляет только скалярные `phone` + `contact_name`
(строки 167-173). JSONB `deals.phones` (доп. телефоны субъекта) остаётся нетронутым → ПДн
сохраняются после удаления.

**Дефект 2 — email-путь пропускает deals.** При erasure только по email (`phone === null`) для
deals выполняется `whereRaw('FALSE')` (строки 157-160) → deals субъекта вообще не анонимизируются,
если известен только email.

**Также к проверке:** `deals.utm_*` и `supplier_leads.raw_payload` — последний перезаписывается
в `{erased:true}` (OK, строки 117-124); utm в deals может содержать косвенные ПДн (ниже приоритет).

**Последствие:** неполное удаление субъекта = нарушение ст.21 152-ФЗ (право на удаление).

**Фикс (TDD):** расширить `eraseSubject()` — (1) при анонимизации deals чистить/обнулять
`phones` JSONB; (2) для email-пути найти deals по вхождению email/phone в JSONB-полях, не `FALSE`.
Тест: создать deal с заполненным `phones`, выполнить erasure, проверить что `phones` очищен.

---

## F-T1 — SSRF `/api/webhooks/test` → 🟢 ЗАКРЫТ (базово)

**Роут:** `app/routes/web.php:204-207` — `/api/webhooks/test` внутри
`Route::middleware(['auth:sanctum', 'tenant'])` → аутентифицирован.

**Гард:** `app/app/Support/WebhookUrlGuard.php` — резолвит хост во ВСЕ A/AAAA, блокирует
приватные/зарезервированные IP (`FILTER_FLAG_NO_PRIV_RANGE | NO_RES_RANGE`), cloud-metadata
169.254.169.254, IPv6 loopback/link-local/ULA/IPv4-mapped. Зовётся и на save (`update()`), и на
`test()` (`WebhookSettingsController.php:64, 133`).

**Вердикт:** базовый SSRF (литеральный внутренний URL + info-leak) закрыт. **Остаток (низкий
приоритет):** DNS-rebinding/TOCTOU — гард резолвит при проверке, `Http::post($url)` резолвит
заново при запросе; публичный хост может переразрешиться в приватный между проверкой и запросом.
Усиление: пинить проверенный IP в исходящий запрос (resolve→connect-by-IP с Host-заголовком).

---

## Сводка

| Находка | Статус | Действие |
|---|---|---|
| F-T2 admin-гейт | 🟡 прикрыт nginx Basic Auth | харден (SSO Yandex 360) — блок Б-1/DO-4; атрибуция админа |
| F-P1 erasure | 🔴 открыт | фикс `PdErasureService` (TDD) — deals.phones JSONB + email-путь |
| F-T1 SSRF | 🟢 закрыт | опц. усиление от DNS-rebinding |

**Метод:** статика по local==prod коду (прод-паритет app сверен хешами) + live-чтение nginx-конфига
прода. Не делалось: полный байт-в-байт сан всего app-дерева; live-DAST реальными запросами к
эндпоинтам.
