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. Но unifiedregister/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'ами.