Недавно я розбирала security bug, який спочатку виглядав як проблема middleware.
Правило verification було цілком правильним для public/cabinet частини: певні дії там справді мали бути доступні лише після підтвердження email.
Але admin UI використовував ті самі endpoints. Через це адміністративний сценарій випадково проходив через middleware, призначений для зовсім іншої authorization boundary.
Middleware не був неправильним. Неправильним було місце, де проходила межа.
Правильне security-rule може стати неправильним у чужому контексті
Public / cabinet flow authenticated | vemail verified | vallowed action Admin flow authenticated admin | vОбидва правила можуть бути правильними одночасно.
Cabinet може залежати від email verification, а admin access - від адміністративної ролі або permission.
Проблема виникає тоді, коли один endpoint намагаються зробити спільним для обох flows, але залишають middleware тільки від одного з них.
Route group часто непомітно стає security boundary
Route::middleware([ 'auth', 'verified',])->group(function (): void { Route::get( '/profile/resources', ResourceIndexAction::class, );});Route group легко сприймати як спосіб не дублювати auth, prefix або controller namespace.
Але з security-погляду він ще й задає набір передумов для всіх endpoints усередині.
Якщо endpoint входить у цю групу, він фактично погоджується з її правилами доступу.
Спільний endpoint не завжди означає спільну authorization policy
Повторне використання endpoint виглядає привабливо: менше routes, менше controllers, одна реалізація.
Але якщо public і admin flows мають різні правила доступу, спільний endpoint починає накопичувати умовні винятки.
У якийсь момент код уже не відповідає на просте питання: "хто має право викликати цю дію?".
Симптом: починаються bypass для окремих ролей
if ($user->isAdmin()) { return $next($request);} if (! $user->hasVerifiedEmail()) { abort(403);}Такий fix іноді потрібен, але для мене це сигнал зупинитися й перевірити архітектурну межу.
Якщо middleware починає знати про всі ролі та всі винятки системи, можливо, воно вже обслуговує кілька різних security contexts одночасно.
Я відділяю authentication, verification і authorization
Цей кейс добре нагадує те, що я описувала в матеріалі про різницю між authentication та authorization.
Authentication відповідає на питання, хто користувач. Verification підтверджує додатковий стан його облікового запису. Authorization визначає, чи може він виконати конкретну дію.
Ці перевірки можуть стояти поруч у pipeline, але вони не взаємозамінні.
Admin access не повинен випадково успадковувати cabinet prerequisites
Wrong boundary shared endpoint | +-- auth +-- verified +-- maybe admin permission Better boundaries cabinet endpoint | +-- auth +-- verified +-- cabinet policyЦе не означає, що потрібно дублювати бізнес-логіку.
Routes або action classes можуть бути окремими, але нижче обидва flows здатні викликати той самий application service.
Я відділяю transport/security boundary від reuse бізнесової операції.
Один service може бути спільним, навіть якщо endpoints різні
final class CabinetResourceAction{ public function __invoke( Request $request, ResourceService $service, ) { $this->authorize( 'viewFromCabinet', ); return $service->handle( $request->user(), ); }} Route reuse і code reuse - не одне й те саме
Саме це було для мене ключовим у цьому кейсі.
Бажання не дублювати code не означає, що потрібно мати один URL і один middleware stack для всіх contexts.
Можна повторно використати query/service/DTO, але залишити окремі entry points із власними security rules.
Я перевіряю boundary ще до middleware implementation
Перед додаванням middleware я тепер ставлю кілька питань:
- Хто є actor цього endpoint?
- Це public, cabinet чи admin flow?
- Які prerequisites справді належать цьому flow?
- Чи потрібна verification саме для цієї дії?
- Яка permission або policy захищає бізнесову дію?
- Чи не використовується endpoint з іншого security context?
Тести повинні показувати різні security contexts
Cabinet user authenticated + unverified -> blocked authenticated + verified -> allowed Admin user admin permission present -> allowed admin permission missing -> blockedЯкщо тестувати лише cabinet endpoint, admin regression може залишитися невидимою.
І навпаки, тест "admin отримує 200" без перевірки permission boundary може приховати занадто широкий доступ.
Тести мають фіксувати не тільки результат, а й те, чому доступ дозволений або заборонений.
Security bug інколи виправляється переміщенням endpoint, а не зміною middleware
Це найважливіша частина кейсу.
Якщо verification middleware правильно захищає cabinet group, послабити його заради admin UI може бути неправильним fix.
Безпечніше винести admin entry point у його власну route group і залишити обидві boundaries чіткими.
Моя перевірка, коли middleware раптом блокує правильний flow
- Перевіряю, для якого user flow middleware створювався спочатку.
- Дивлюся, хто ще використовує endpoint.
- Відділяю account prerequisite від action authorization.
- Перевіряю route group, prefix і middleware stack.
- Шукаю role-specific bypass як можливий сигнал неправильної boundary.
- Перевіряю, чи можна розділити entry points і залишити спільний service.
- Додаю окремі security tests для кожного context.
Middleware відповідає за правило всередині межі, але не визначає правильність самої межі
Після цього кейсу я набагато обережніше дивлюся на security middleware як на остаточну відповідь.
Middleware може бути абсолютно правильним для свого context і водночас ламати інший flow, якщо той випадково потрапив під ту саму route group.
Тому коли access rule виглядає дивно, я перевіряю не тільки condition. Я перевіряю, чи endpoint стоїть на правильній authorization boundary.
Іноді security fix - це не послабити middleware. Це повернути endpoint у ту boundary, якій він насправді належить.



