0% прочитано

Middleware може бути правильним, але стояти не на тій межі

Security bug не завжди означає, що middleware написаний неправильно. Іноді правило правильне для одного типу користувачів, але endpoint помилково живе в route group іншої authorization boundary.

21 жовтня 2026 р. 10 хв читанняЛаравель

Недавно я розбирала security bug, який спочатку виглядав як проблема middleware.

Правило verification було цілком правильним для public/cabinet частини: певні дії там справді мали бути доступні лише після підтвердження email.

Але admin UI використовував ті самі endpoints. Через це адміністративний сценарій випадково проходив через middleware, призначений для зовсім іншої authorization boundary.

Middleware не був неправильним. Неправильним було місце, де проходила межа.

Коли middleware блокує легітимний сценарій, я спочатку питаю не "як його обійти?", а "чи цей endpoint взагалі належить до цієї route boundary?".

Правильне 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

Middleware stack задає не тільки технічний pipeline
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 для окремих ролей

Локальний bypass може приховати неправильну route boundary
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

Різні flows отримують власні передумови
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 різні

Security boundary окрема, application logic спільна
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

Тестова матриця відображає boundary, а не один happy path
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, якій він насправді належить.