Безпечні редіректи

Відкриті редіректи (?next=https://evil.com) — типова вразливість. Concept Core обмежує «назад» і fallback-URL лише внутрішніми адресами застосунку через StorePreviousUrlMiddleware і ResponseFactory::back().

Див. також: Відповіді → редіректи, Middleware → StorePreviousUrlMiddleware, помилки валідації.

Класи ядра

КласРоль
StorePreviousUrlMiddleware Ланцюжок URL у сесії + Concept\Core\Http\Requests\RequestAttribute::SAFE_BACK_URL
ResponseFactory::back() Referer → safe back → fallback, з перевіркою isInternalUrl()
ResponseFactory::redirectByName() Редірект за іменем маршруту через UrlGenerator::uri()
HandleValidationExceptionMiddleware HTML: flash + back() після ValidationException

StorePreviousUrlMiddleware

Middleware веде в сесії два URL:

  • SessionKey::CURRENT_URL (_url_current) — остання GET-сторінка (не AJAX)
  • SessionKey::PREVIOUS_URL (_url_previous) — попередня GET-сторінка

На GET (не AJAX) middleware зберігає повний URI запиту ((string) $request->getUri(), включно з query string). Якщо URI відрізняється від поточного в сесії — попередній CURRENT_URL стає PREVIOUS_URL, новий URI — CURRENT_URL.

На кожен запит у PSR-7 атрибут Concept\Core\Http\Requests\RequestAttribute::SAFE_BACK_URL (safe_back_url):

  • GETPREVIOUS_URL (сторінка, з якої прийшли)
  • POST, PUT, PATCH, DELETE та інші не-GET — CURRENT_URL (сторінка форми)

Типове місце в стеку — після CSRF, перед ShareViewDataMiddleware:

$router->lazyMiddleware(HandleValidationExceptionMiddleware::class);
$router->lazyMiddleware(VerifyCsrfTokenMiddleware::class);
$router->lazyMiddleware(StorePreviousUrlMiddleware::class);
$router->lazyMiddleware(ShareViewDataMiddleware::class);

AJAX-запити (HttpHeader::X_REQUESTED_WITH = XMLHttpRequest) не оновлюють ланцюжок URL, щоб фонові poll-запити не зламали «назад» після сабміту форми. Атрибут SAFE_BACK_URL на таких запитах усе одно виставляється з сесії.

ResponseFactory::back()

Контролер або middleware після валідації часто робить redirect назад:

return $this->response->back();
return $this->response->back(
    status: HttpStatusCode::FOUND,
    fallback: '/cabinet'
);

Алгоритм вибору цілі:

  1. Referer — якщо заголовок не порожній (пріоритет №1)
  2. SAFE_BACK_URL — з атрибута запиту, лише коли Referer відсутній
  3. Перевірка — обраний URL проходить isInternalUrl(); інакше — fallback
  4. Fallback — другий аргумент методу (за замовчуванням /)

Зовнішній Referer (інший host) не використовується — спрацьовує fallback. Статус за замовчуванням — HttpStatusCode::FOUND (302).

Що вважається «внутрішнім»

Приватний метод ResponseFactory::isInternalUrl():

  • Порожній рядок — відхиляється
  • Без host у parse_url() — дозволено лише якщо рядок починається з /
  • З host — лише якщо host строго збігається з host поточного PSR-7 запиту
  • Зовнішні домени, //evil.com, javascript: тощо — відхиляються → fallback

Перевірка застосовується лише в back(). Методи redirect($url) і redirectByName() не фільтрують ціль — не передавайте туди ненадійний input користувача.

Три способи редіректу

МетодКолиПеревірка URL
back() Повернення на форму, помилка валідації Так (isInternalUrl)
redirectByName() PRG після POST, фіксована ціль Ні (шлях з генератора маршрутів)
redirect($url) Відомий статичний шлях Ні (відповідальність розробника)

Після помилок валідації

HandleValidationExceptionMiddleware для HTML-запитів робить flash помилок і back() — користувач повертається на форму з тим самим URL, без довільного redirect($_GET['return']).

Див. Запити → помилки валідації та Middleware.

Іменовані маршрути

Для фіксованих цілей безпечніше іменований redirect — шлях будує UrlGeneratorInterface::uri():

return $this->response->redirectByName('cabinet.home');
return $this->response->redirectByName('admin.user.edit', ['id' => $id]);

Використовуйте back() для UX «повернутися на форму», а redirectByName() — коли ціль завжди відома (login, dashboard).

Не приймайте URL редіректу з ненадійного input користувача без whitelist. Якщо потрібен параметр ?next=, порівнюйте з allowlist шляхів або імен маршрутів.