Безпечні редіректи
Відкриті редіректи (?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):
- GET —
PREVIOUS_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'
);
Алгоритм вибору цілі:
- Referer — якщо заголовок не порожній (пріоритет №1)
- SAFE_BACK_URL — з атрибута запиту, лише коли Referer відсутній
- Перевірка — обраний URL проходить
isInternalUrl(); інакше — fallback - 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 шляхів або імен маршрутів.