# Security Audit — 2026-06-07

**Period:** статический аудит кодовой базы на коммите `c64dcff` (изменения с предыдущего аудита 2026-06-01)

## Findings

| Category | Finding | Severity | File / Config |
|---|---|---|---|
| Secrets / Supply-chain | Секреты (`BETTER_AUTH_SECRET`, `ADMIN_PASSWORD`) переданы как Docker build ARG → попадают в историю слоёв образа | high | `Dockerfile:14-20` |
| SSRF | DNS-rebinding / multi-record TOCTOU: `safeFetch` проверяет один резолв, `fetch()` резолвит повторно без закрепления IP | medium | `src/lib/mcp/safe-fetch.ts:53-81` |
| Misconfiguration | Отсутствуют HTTP security headers (HSTS, X-Frame-Options, CSP, nosniff, Referrer-Policy) | medium | `next.config.ts` |
| Infra hardening | Контейнер запускается от root (нет `USER`) | medium | `Dockerfile` |
| Auth (brute-force) | Невалидные apiKey на `/api/mcp/[apiKey]` не подпадают под rate limit (лимит считается только после успешного поиска endpoint) | low | `src/app/api/mcp/[apiKey]/route.ts:48-87` |
| Crypto (timing) | `CRON_SECRET` сравнивается через `!==`, не constant-time | low | `src/app/api/cron/refresh-tokens/route.ts:250` |
| Injection (by-design) | «GAQL injection» в Google Ads — запрос идёт по OAuth-токену самого пользователя в его же аккаунт; границы доверия не пересекаются | low | `src/lib/mcp/google-ads.ts:136-176` |

## Resolved since previous audits

Подтверждено исправление ранее заведённых находок:

- **Crypto**: `crypto.ts` — случайный IV на каждое шифрование, AES-256-GCM с auth tag, PBKDF2 (100k). ✓
- **OAuth state**: `oauth-state.ts` — HMAC проверяется через `timingSafeEqual` (constant-time), есть CSRF-токен + cookie-сверка в `oauth-flow.ts`. ✓
- **Token leakage**: во всех `*-oauth.ts` и в OTel-спанах логируются только статус-коды, не токены (фикс Threads-спанов распространён корректно). ✓
- **Authorization (IDOR / роли)**: мутации защищены `requireProjectOwner` / `canManageMembers` (owner|admin); чтение — `requireProjectMember`; админ-роуты — `requireAdmin`. Прежняя находка «Member может выполнять все write-операции» закрыта. ✓
- **Dependencies**: `pnpm audit --prod` — уязвимостей нет. ✓
- **Secrets в репозитории**: только `.env.example` с плейсхолдерами; `.env` в `.gitignore`. ✓

## Recurring Issues

- **SSRF**: базовая защита (блок приватных диапазонов + резолв DNS) присутствует с прошлого аудита, но остаточный TOCTOU/rebinding-вектор не закрыт. Вторая итерация подряд, где SSRF фигурирует — теперь уже как остаточный риск, а не как полное отсутствие защиты.

## Hypotheses

- **Rate limit на невалидные apiKey**: ключи — cuid2 (~120 бит), брутфорс практически невозможен, поэтому отсутствие лимита на 401 пока не дотягивает до issue. Сигнал к мониторингу: всплеск 401 на `/api/mcp/*` в SigNoz → пересмотреть.
- **CRON_SECRET timing**: high-entropy секрет, сетевой timing-attack нереалистичен; defense-in-depth, не заводим issue.

## Recommendations

1. **Найдено**: мастер-секреты в Docker build ARG → **фикс**: убрать ARG, передавать только как runtime-env или BuildKit secret mounts; ротировать ключи при необходимости → **критерий**: `docker history` не содержит значений секретов. (issue #87)
2. **Найдено**: DNS-rebinding в `safeFetch` → **фикс**: резолвить один раз, проверять все адреса, подключаться к закреплённому IP через кастомный undici lookup → **критерий**: тест на rebinding блокирует запрос. (issue #88)
3. **Найдено**: нет security headers → **фикс**: `headers()` в `next.config.ts` (HSTS/XFO/CSP/nosniff/Referrer-Policy) → **критерий**: заголовки присутствуют, SSE не сломан. (issue #89)
4. **Найдено**: контейнер от root → **фикс**: `USER node` + корректные права на `/app` → **критерий**: `id` в контейнере не uid=0, приложение стартует. (issue #90)
