Qadam Roadmap
проектdocs/security-review.md

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

Обновлён 1 апр. 2026 г., 12:41 · 0 комментариев

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 года
  • Связанные документы:

Этот файл не является каноническим 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'ами.