26 окт. 2025 г.·4 мин. чтения

Уязвимость открытого перенаправления в callback'ах авторизации: как её исправить

Узнайте, как возникает уязвимость открытого перенаправления в callback'ах авторизации, как её используют злоумышленники и как исправить перенаправления с помощью жёстких allowlist'ов и безопасного парсинга URL.

Уязвимость открытого перенаправления в callback'ах авторизации: как её исправить

Почему перенаправления в потоках входа могут стать уязвимостью

Перенаправление — это инструкция для браузера: «перейди на эту страницу». В потоках входа перенаправления делают процесс бесшовным: ваше приложение отправляет пользователя войти, а затем возвращает его к тому, что он хотел посмотреть.

Шаг «вернуть меня» обычно хранится в параметре вроде returnTo, next или redirect. Если ваше приложение принимает там любую URL, у вас возникает уязвимость открытого перенаправления.

Это часто встречается в прототипах, потому что перенаправления — лёгкий способ сделать демонстрацию гладкой. Код, сгенерированный AI или написанный в спешке, часто ориентируется на «сделай так, чтобы работало» и пропускает скучные правила безопасности.

Риск больше, чем «пользователь попадёт не туда». Ваш домен становится доверенной отправной точкой для перенаправления людей на сайт, контролируемый атакующим. Ссылка выглядит легитимной, потому что начинается с вашего реального домена и часто показывает привычный экран входа.

Реалистичный фишинговый сценарий выглядит так: кто-то кликает по ссылке «Войти» из письма или чата, попадает на вашу реальную страницу входа, входит в аккаунт, а затем перенаправляется на поддельную страницу, где просят «войти снова», ввести код MFA или подтвердить платёжные данные. Многие пользователи не заметят подвоха, потому что до финального шага всё выглядело нормально.

Что такое открытые перенаправления и callback'и авторизации

Открытое перенаправление — это когда ваше приложение позволяет недоверенному вводу решать, куда отправить пользователя дальше. Классический пример — URL вроде https://yourapp.com/redirect?to=..., где to может указывать на любой сайт.

Callback авторизации — это страница, на которую провайдер идентификации возвращает пользователя после входа. После того как пользователь входит через Google, GitHub или другого провайдера, провайдер отправляет его обратно в ваше приложение на callback-URL, чтобы приложение могло завершить вход и создать сессию.

Проблемы начинаются, когда вы комбинируете эти два механизма.

Типичная схема:

  • Пользователь пытается попасть на /billing.
  • Ваше приложение отправляет его на /login?next=/billing.
  • После входа ваш callback читает next (или returnUrl, redirect, continue) и отправляет пользователя туда.

Если callback принимает next=https://evil.example, вы встроили открытое перенаправление в самую доверенную часть продукта.

Влияние не ограничивается фишингом. Перенаправления внутри OAuth-потоков могут увеличить зону поражения, когда команды передают чувствительные значения через URL на ранних этапах разработки. Даже когда вы «только» перенаправляете OAuth code, вы можете пробросить данные через историю браузера, логи, заголовки referrer или просто посадить пользователя на страницу, созданную, чтобы обманом выманить доступ.

Распространённые рискованные шаблоны перенаправлений

Большинство ошибок с перенаправлениями начинаются одинаково: команда хочет «вернуть на то место, где был пользователь», поэтому прокидывает параметр перенаправления и считает его безопасным.

Сигналы тревоги для поиска:

  • Любой query-параметр, управляющий навигацией после входа (next, returnTo, redirect, url), который используется напрямую в HTTP-редиректе или window.location.
  • Код, принимающий полные URL (https://example.com) вместо внутренних путей (/dashboard).
  • Цели перенаправления, приходящие из localStorage, cookies или заголовков и воспринимаемые как доверенные.
  • «Валидация», которая лишь проверяет startsWith('/').
  • Множественные шаги декодирования/нормализации, из‑за которых непонятно, что именно было проверено, а что использовано.

Две крайних ситуации часто подводят команды:

Protocol-relative URL: значения вроде //evil.com выглядят как путь, но браузеры трактуют их как «используй текущую схему и перейди на evil.com». Простая проверка startsWith('/') пропустит это.

Закодированные URL: атакующие могут спрятать ту же хитрость в кодировке. %2F%2Fevil.com становится //evil.com после декодирования. Если вы валидируете до декодирования или декодируете несколько раз в разных местах, вы можете одобрить одну строку и перенаправить на другую.

Как злоумышленники используют открытые перенаправления

Атакующим нравятся открытые перенаправления, потому что они могут «заимствовать» доверие к вашему домену. Жертва видит ваш реальный сайт в адресной строке, входит в систему, а в конце её отправляют на вредоносный ресурс.

Очень распространённая атака выглядит так:

  1. Злоумышленник рассылает ссылку на ваш реальный домен с параметром перенаправления, например ?next=https://evil.example.

  2. Ваше приложение показывает реальную страницу входа.

  3. После входа ваше приложение перенаправляет пользователя на сайт атакующего.

  4. Сайт атакующего показывает правдоподобный экран «сессия истекла» или «подтвердите аккаунт» и перехватывает учётные данные или коды MFA.

OAuth усугубляет проблему, если ваш callback-эндпоинт обменивает или обрабатывает коды/токены, а затем сразу же перенаправляет по входному параметру. Даже если данные недолговечны, короткого окна достаточно.

Реалистичный пример: ссылка входа, которая уводит пользователей

Получите исправления быстро
Большинство проектов завершается в пределах 48–72 часов с экспертной проверкой.

В прототипе часто добавляют параметр returnTo, чтобы вход выглядел аккуратно.

Обычная ссылка может быть:

/login?returnTo=/billing

Баг появляется, когда returnTo трактуют как «любой URL», а не как «безопасный путь внутри нашего приложения».

Тогда также сработает:

/login?returnTo=https://attacker.example/fake-dashboard

Ничто не ломается. Пользователь успешно входит, а затем попадает на сайт, который выглядит как ваш продукт, но им не является. С точки зрения пользователя, вход сработал, поэтому следующий экран кажется доверенным.

Урок прост: функция «вернуть туда, где вы были» должна принимать только безопасные, ожидаемые назначения. Если она может указывать на внешний URL — это открытая дверь.

Более безопасная модель: относительные пути и жёсткие allowlist'ы

Самый безопасный подход — преднамеренно скучный: воспринимать место назначения после входа как внутренний путь, а не полный URL.

Правило 1: принимайте относительные пути, а не полные URL

Принимайте только значения вроде /settings или /billing. Избегайте принятия https://... и явно отвергайте protocol-relative значения вроде //....

Полезная база: требуйте ведущий одиночный / и отвергайте всё, что начинается с //.

Правило 2: валидируйте по жёсткому allowlist'у

Даже если вы принимаете только относительные пути, может быть полезно ограничить, куда именно можно попасть после авторизации. Allowlist предотвращает неловкие или рискованные назначения вроде циклов /logout, маршрутов, запускающих чувствительные действия, или страниц, доступ к которым должен быть ограничен по ролям.

Держите список маленьким. Разрешите несколько известных безопасных маршрутов (или несколько безопасных префиксов) и по умолчанию перенаправляйте всё остальное на безопасную страницу вроде /dashboard.

Нормализуйте и парсите до принятия решения

Нормализуйте ввод один раз: обрежьте пробелы и декодируйте percent-encoding один раз. Затем валидируйте полученный путь. Избегайте двойного декодирования или валидации одного представления и перенаправления по другому.

Делайте отказ «скучным»

Если значение отсутствует или недействительно, игнорируйте его и перенаправляйте на известный безопасный адрес. Логируйте отклонения, чтобы замечать попытки сканирования и сломанный клиентский код.

Пошагово: как исправить обработку перенаправлений в прототипе

Узнайте сначала, что сломано
Начните с бесплатного аудита кода, чтобы знать обо всех проблемах, прежде чем вносить изменения.

Логика перенаправлений обычно разбросана по middleware, обработчикам callback'ов и UI. Самый быстрый способ сделать её безопасной — считать каждый пункт назначения недоверенным вводом и централизовать валидацию.

  1. Инвентаризируйте все источники перенаправлений: query-параметры (next, returnTo, redirect, callback, continue), cookies, localStorage и любое auth-middleware, которое «запоминает», куда пользователь шел.

  2. Выберите правило: для большинства приложений принимайте только относительные пути. Если вам действительно нужны внешние перенаправления (редко), разрешайте только короткий список точных origin'ов, которые вы контролируете.

  3. Нормализуйте один раз: обрежьте, декодируйте один раз и отвергайте управляющие символы.

  4. Валидируйте строго:

  • Требуйте ведущий одиночный /.
  • Отвергайте //, любые схемы вроде http: или javascript:, и обратные слеши (\\\\) включая закодированные обратные слеши.
  • Отвергайте обходы типа .. и нулевые байты.
  • Если вы разрешаете полные URL, требуйте точного совпадения origin с allowlist'ом.
  1. Перенаправляйте и логируйте: при ошибке отправляйте пользователя на безопасный дефолт и фиксируйте отклонённое значение.

Распространённые ошибки, которые сохраняют уязвимость

Большинство неудачных фиксов выглядят «валидированными», но продолжают трактовать перенаправления как простые строки.

Типичные ловушки:

  • Allowlist по подстроке (например, проверка includes('mydomain.com')). Атакующий может использовать mydomain.com.evil.com или спрятать доверенный текст в пути/запросе.
  • Валидация только на клиенте. Клиентская проверка улучшает UX, но сервер должен быть финальным контролём.
  • Валидация одного параметра, но перенаправление с другим из‑за помощников фреймворка или приоритета параметров.
  • Непоследовательная нормализация, валидация до декодирования или многократное декодирование.

Также следите за мыслью «мы же установили это раньше, значит это безопасно». Если значение хранится в localStorage, скрытом поле или cookie, злоумышленник всё ещё может его изменить или пропустить страницу, которая его установила.

Быстрые проверки перед релизом

Проверьте перенаправления при входе
Отправьте код — мы отметим открытые перенаправления и рискованную обработку callback'ов авторизации.

Большинство проблем с перенаправлениями можно поймать парой простых тестов. Цель проста: ни одно значение, контролируемое пользователем, не должно отправлять браузер на неожиданный домен, а неизвестные значения должны приземляться в безопасном месте.

Протестируйте параметр, который ваше приложение использует после входа (next, redirect, returnTo, callbackUrl). Убедитесь, что обычный внутренний путь работает, затем попробуйте вводы, которые часто проходят мимо наивных проверок:

  • https://example.com (должно отклоняться)
  • //evil.com и %2F%2Fevil.com (должно отклоняться)
  • \\\\evil.com (некоторые фреймворки нормализуют это неожиданным образом)
  • Неизвестный внутренний маршрут вроде /definitely-not-real (должен откатиться к безопасному дефолту)

Повторите те же тесты как для клиентского роутинга, так и для серверных эндпоинтов, которые завершают сессии или обрабатывают OAuth-callback'и. Атакующие воспользуются самым слабым звеном.

Следующие шаги: привести перенаправления в порядок и сделать их безопасными

Ошибки открытого перенаправления редко живут в одном месте. В прототипах они возникают везде, где приложение пытается быть полезным после входа: guards роутов, middleware, перебрасывающем неаутентифицированных пользователей, OAuth callback handlers, invite-ссылках и онбординговых потоках.

Хорошее конечное состояние — скучно: каждое перенаправление либо известный безопасный относительный путь, либо (если действительно нужно) абсолютный URL, совпадающий с коротким allowlist'ом ваших origin'ов. Всё остальное игнорируется и заменяется безопасным дефолтом.

Если вы имеете дело с кодовой базой, сгенерированной AI, и хотите второе мнение, FixMyMess (fixmymess.ai) фокусируется на диагностике и исправлении таких проблем с авторизацией и перенаправлениями, а также связанных вопросов: утечки секретов и небезопасные паттерны, которые работают в демо, но ломаются в продакшене.