fix(tradein): account-бренд приоритетен над ?brand= — cross-brand leak (#765) #780

Merged
bot-reviewer merged 1 commit from feat/765-brand-account-priority into main 2026-05-30 16:29:12 +00:00
Collaborator

Summary

useActiveBrandSlug теперь отдаёт приоритет me.brand (account binding) над URL-параметром ?brand=. ?brand= срабатывает только когда у аккаунта нет своего бренда (демо/превью). Обновлён JSDoc.

Why (security)

?brand=<slug> безусловно перекрывал me.brand → любой залогиненный пользователь мог отрендерить чужой бренд (лого/цвета/футер/pdf-дисклеймер) и сгенерировать brand-skinned PDF чужого бренда (чейн с IDOR #690/#710). В prod бренд должен определяться аккаунтом.

Test plan

  • npx tsc --noEmit → 0 ошибок
  • npm run build → ✓ Compiled successfully, 10/10 pages
  • getBrandSlugFromUrl() теперь под условием !accountSlug (после проверки account)
  • Smoke: praktika login (me.brand=praktika) + ?brand=acme → остаётся praktika; demo-юзер без бренда + ?brand=praktika → praktika (qa)

Follow-up (вне scope, заведено как замечание)

Backend /api/v1/brand/{slug} должен дополнительно reject mismatched slug для аккаунта (defense-in-depth) — отдельный backend-issue.

Refs #765

## Summary `useActiveBrandSlug` теперь отдаёт приоритет `me.brand` (account binding) над URL-параметром `?brand=`. `?brand=` срабатывает **только** когда у аккаунта нет своего бренда (демо/превью). Обновлён JSDoc. ## Why (security) `?brand=<slug>` безусловно перекрывал `me.brand` → любой залогиненный пользователь мог отрендерить чужой бренд (лого/цвета/футер/pdf-дисклеймер) и сгенерировать brand-skinned PDF чужого бренда (чейн с IDOR #690/#710). В prod бренд должен определяться аккаунтом. ## Test plan - [x] `npx tsc --noEmit` → 0 ошибок - [x] `npm run build` → ✓ Compiled successfully, 10/10 pages - [x] `getBrandSlugFromUrl()` теперь под условием `!accountSlug` (после проверки account) - [ ] Smoke: praktika login (me.brand=praktika) + `?brand=acme` → остаётся praktika; demo-юзер без бренда + `?brand=praktika` → praktika (qa) ## Follow-up (вне scope, заведено как замечание) Backend `/api/v1/brand/{slug}` должен дополнительно reject mismatched slug для аккаунта (defense-in-depth) — отдельный backend-issue. Refs #765
bot-frontend added 1 commit 2026-05-30 16:23:22 +00:00
?brand=<slug> безусловно перекрывал me.brand → любой залогиненный мог
отрендерить чужой бренд (лого/цвета/PDF-дисклеймер). Теперь me.brand
приоритетен; ?brand= разрешён только когда у аккаунта нет своего бренда
(демо-превью не сломан).

Refs #765
bot-reviewer approved these changes 2026-05-30 16:29:01 +00:00
bot-reviewer left a comment
Collaborator

APPROVE

Brand cross-leak fix (frontend security #765), useBrand.ts (содержит useActiveBrandSlug).

Correctness — реализует minimal-safe версию issue дословно:

  • Было: ?brand= URL-param безусловно перевешивал me.brand.
  • Стало: accountSlug = me?.brand?.trim().toLowerCase(); if (accountSlug) return accountSlug; return getBrandSlugFromUrl(). Account-бренд (server-authoritative из /me) приоритетен; ?brand= honored только когда у аккаунта нет бренда (демо/превью).
  • me.brand непустой → ?brand=other игнорируется → cross-brand leak закрыт (чейн с IDOR #690/#710 brand-skinned PDF). me.brand пустой → ?brand= работает (демо не сломано). JSDoc обновлён. DoD: getBrandSlugFromUrl под !accountSlug ✓.
  • tsc 0, build ✓ 10/10. Усиливает целевое поведение #657 brand-by-account — praktika login не задет.

Scope/security — только useBrand.ts; нет backend (mismatch-reject — задокументированный follow-up), нет изменения Brand-интерфейса, нет secrets, нет self-extending триггера.

Verdict: APPROVE (Praktika security #765). Smoke (praktika+?brand=acme→praktika; demo+?brand=praktika→praktika) → qa.

<!-- gendesign-review-bot: sha=5bf26c2 verdict=approve --> ✅ APPROVE Brand cross-leak fix (frontend security #765), `useBrand.ts` (содержит `useActiveBrandSlug`). **Correctness** — реализует minimal-safe версию issue дословно: - Было: `?brand=` URL-param безусловно перевешивал `me.brand`. - Стало: `accountSlug = me?.brand?.trim().toLowerCase()`; `if (accountSlug) return accountSlug; return getBrandSlugFromUrl()`. Account-бренд (server-authoritative из `/me`) приоритетен; `?brand=` honored только когда у аккаунта нет бренда (демо/превью). - `me.brand` непустой → `?brand=other` игнорируется → cross-brand leak закрыт (чейн с IDOR #690/#710 brand-skinned PDF). `me.brand` пустой → `?brand=` работает (демо не сломано). JSDoc обновлён. DoD: `getBrandSlugFromUrl` под `!accountSlug` ✓. - tsc 0, build ✓ 10/10. Усиливает целевое поведение #657 brand-by-account — praktika login не задет. **Scope/security** — только useBrand.ts; нет backend (mismatch-reject — задокументированный follow-up), нет изменения Brand-интерфейса, нет secrets, нет self-extending триггера. **Verdict: ✅ APPROVE** (Praktika security #765). Smoke (praktika+`?brand=acme`→praktika; demo+`?brand=praktika`→praktika) → qa.
bot-reviewer merged commit 238f14a0d7 into main 2026-05-30 16:29:12 +00:00
bot-reviewer deleted branch feat/765-brand-account-priority 2026-05-30 16:29:12 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: lekss361/gendesign#780
No description provided.