# Security review — 2026-03-28 | working tree `/data/qadam-core`

## Паспорт документа

- Статус документа: historical snapshot
- Актуально на: 28 марта 2026 года
- Владелец: backend/platform-команда
- Пересмотр: только при необходимости ссылаться на этот review как на исторический working-tree snapshot
- Область применения: отдельный исторический security/code review незакоммиченного рабочего дерева `qadam-core` на 28 марта 2026 года
- Связанные документы:
  - [Security review канонического audit-слоя](./audits/security-review.md)
  - [Текущее состояние](./project/current-state.md)
  - [Требования к registration API](./product/requirements-api-registration.md)

Этот файл не является каноническим operational security-runbook. Это отдельный исторический review working tree на момент завершения пакета `requirements-api-registration`.

## Ревью — 2026-03-28 | /data/qadam-core working tree

**Суть изменения:** Текущий working tree завершает `requirements-api-registration` и затрагивает auth/password-reset, seller profile, Telegram verify, upload endpoint и admin seller status management. `pnpm check-types` проходит, но `pnpm test` сейчас падает, а в security-чувствительных потоках остаются несколько дефектов уровня contract break и account takeover.

---

### Замечания

#### [КРИТИЧНО] — Password reset token выдаётся в HTTP-ответе и логируется как transport secret
- **Файл:** `apps/api/src/modules/auth/auth.service.ts` (строка 242), `apps/api/src/modules/auth/auth.controller.ts` (строка 232)
- **Категория:** Безопасность
- **Описание:** `forgotPassword()` возвращает `token` прямо в API-ответе и одновременно пишет reset secret в warn-логи (`resetToken` для email flow, `resetCode` для SMS flow). При этом `resetPassword()` требует дополнительного подтверждения только для `SMS_CODE`; токен `EMAIL_LINK` можно использовать напрямую. В результате любой, кто знает email жертвы, может вызвать публичный `/auth/forgot-password`, получить reset token из ответа и сразу сменить пароль без доступа к почте. Логирование тех же секретов дополнительно расширяет поверхность компрометации.
- **Направление исправления:** Reset secrets нельзя возвращать в HTTP-ответ и писать в application logs. Email flow должен доставлять токен только out-of-band, а клиенту можно отдавать лишь безопасный session handle/метаданные шага.

---

#### [ВЫСОКИЙ] — Legacy seller profile ошибочно делает contact phone/email глобально уникальными как account credentials
- **Файл:** `apps/api/src/modules/seller/seller.service.ts` (строка 126), `apps/api/src/modules/seller/repositories/seller.repository.ts` (строка 129), `apps/api/src/modules/auth/repositories/auth.repository.ts` (строка 186)
- **Категория:** Корректность
- **Описание:** `createProfile()` и `updateProfile()` проверяют `dto.phone`/`dto.email` через `Account.phone`/`Account.email`, то есть как глобально уникальные credentials. Но unified `register/seller` сохраняет `contactPhone`/`contactEmail` как отдельные профильные поля, а продуктовый контракт прямо говорит, что они предзаполняются из account-полей, но могут отличаться. Это создаёт рассинхрон между legacy seller profile API и новым registration flow и запрещает валидные сценарии, когда контактные данные продавца совпадают с уже существующим account-каналом другой сущности.
- **Направление исправления:** Нужно развести уникальность account credentials и профильных contact fields и привести legacy/update flow к тому же контракту, что и unified registration.

---

#### [СРЕДНИЙ] — Telegram verification code остаётся re-usable при гонке параллельных запросов
- **Файл:** `apps/api/src/modules/seller/seller.service.ts` (строка 289), `apps/api/src/modules/seller/repositories/seller.repository.ts` (строка 495)
- **Категория:** Безопасность
- **Описание:** `verifyTelegram()` сначала читает код и проверяет `used/expiresAt`, а затем `bindTelegram()` в отдельной транзакции просто делает `update({ where: { code }, data: { used: true } })`. Два параллельных запроса с одним и тем же кодом могут пройти pre-check до записи `used = true`, после чего оба завершатся успешно. Для одноразового verification code это нарушение single-use semantics.
- **Направление исправления:** Поглощение Telegram code должно быть атомарным условным write-path'ом, где второй запрос падает до bind/update seller.

---

#### [СРЕДНИЙ] — Guard маскирует BLOCKED/UNDER_REVIEW аккаунты под generic 401 invalid token
- **Файл:** `apps/api/src/common/guards/jwt-auth.guard.ts` (строка 34), `apps/api/src/modules/auth/auth.service.ts` (строка 194), `apps/api/src/common/filters/domain-exception.filter.ts` (строка 28)
- **Категория:** Корректность
- **Описание:** `AuthService.getMe()` различает `ACCOUNT_BLOCKED` и `ACCOUNT_UNDER_REVIEW` и через `DomainExceptionFilter` должен отдавать `403` с доменным `errorCode`. Но `JwtAuthGuard` ловит эти исключения и превращает их в `UnauthorizedException('Invalid or expired token')`. В итоге protected routes теряют реальную причину отказа, расходятся с login/refresh flow и ломают корректную клиентскую реакцию на блокировку или ревью аккаунта.
- **Направление исправления:** Guard должен сохранять доменные ошибки статуса аккаунта или маппить их в согласованный HTTP/error contract, а не схлопывать всё в generic invalid token.

---

#### [СРЕДНИЙ] — Repo-level test gate сейчас красный из-за устаревшего JwtAuthGuard spec
- **Файл:** `apps/api/src/common/guards/jwt-auth.guard.spec.ts` (строка 31), `apps/api/src/common/guards/jwt-auth.guard.ts` (строка 20)
- **Категория:** Качество
- **Описание:** `pnpm -C /data/qadam-core test` падает на `jwt-auth.guard.spec.ts`: spec продолжает создавать guard без `AuthService` и проверяет синхронный `canActivate()`, тогда как реальная реализация стала `async` и зависит от `authService.getMe()`. В результате новый hardening на access-token invalidation уже поменял прод-поведение, но regression coverage для него отсутствует, а основной quality gate репозитория сломан.
- **Направление исправления:** Нужно обновить spec под текущий async-контракт guard и добавить рабочий regression test на поведение blocked/under-review access token.

---

### Общая оценка
[КРИТИЧНО]
Текущее состояние нельзя считать безопасным для релиза из-за прямого account-takeover риска в password reset flow. Дополнительно working tree уже расходится с обещанными quality gates: `check-types` зелёный, но `pnpm test` красный, а часть новой security-логики не закрыта корректными тестами и атомарными write-path'ами.
