Примечание: Чувствительные значения (коды авторизации, cookies, session ID) заменены на
[REDACTED]. Хост и логика потока сохранены для воспроизводимости.
Краткое резюме
Выявлена и подтверждена уязвимость Cross-Site Request Forgery (CSRF) в потоке привязки Google OAuth сервиса Verisign DomainScope (domainscope.com). Callback /connect/google не проверял параметр state. Злоумышленник мог заставить аутентифицированную сессию жертвы привязать Google-аккаунт злоумышленника к профилю DomainScope жертвы, а затем войти как жертва через Google OAuth. Последствие — полный захват аккаунта без знания пароля жертвы. Сообщено через Bugcrowd в апреле 2018 года в рамках авторизованной программы Verisign bug bounty, проверено и вознаграждено $1,000, отмечено в Verisign Hall of Fame. Находка сделана в ходе участия в программе Bugcrowd в рамках авторизованного тестирования на проникновение.
Контекст — поток привязки OAuth в DomainScope
На момент тестирования Verisign DomainScope поддерживал два связанных потока аутентификации:
- Основной вход: Пользователи могли входить в DomainScope через LinkedIn OAuth.
- Привязка аккаунта в My Profile: Аутентифицированный пользователь мог перейти в My Profile → Link Google Account, чтобы связать Google-идентичность со своим существующим профилем DomainScope. После привязки пользователь мог далее аутентифицироваться в DomainScope через Sign in with Google.
Предполагаемый поток OAuth 2.0 для привязки выглядел так:
- Пользователь нажимает Link Google Account, будучи аутентифицированным в DomainScope.
- Браузер перенаправляется на
https://accounts.google.comдля авторизации Google. - Google перенаправляет обратно на callback DomainScope
GET /connect/google?code=...&state=...с кодом авторизации и значениемstate, которое должно быть привязано к сессии пользователя. - DomainScope обменивает код на токены и привязывает возвращённую Google-идентичность (
email,profile) к текущему аутентифицированному аккаунту DomainScope.
Параметр state определён в RFC 6749 как защита от CSRF для OAuth. При привязке внешней идентичности к уже аутентифицированному аккаунту state критически важен. Без него действие привязки не привязано к пользователю, который его инициировал.
Уязвимость — отсутствие проверки state позволяет CSRF
Проанализирован callback /connect/google и задокументировано, что параметр state не проверялся. Приложение принимало callback, содержащий только code и scope, без требования криптографически случайного state, привязанного к сессии жертвы.
Подтверждённое поведение:
- Перехват callback Google OAuth показал, что запрос можно воспроизвести без значения
state. - Удаление параметра
stateвсё равно приводило к успешной привязке между сессией DomainScope и Google-идентичностью, представленной кодомcode. - На этом endpoint привязки на момент тестирования не наблюдалось ни анти-CSRF токена, ни enforcement
SameSite, ни серверной привязки к сессии.
В привязке OAuth-аккаунтов отсутствие проверки state превращает callback в CSRF-эндпоинт. Злоумышленник может подготовить валидный код авторизации для своего собственного Google-аккаунта и заставить браузер жертвы доставить его в сессию DomainScope жертвы. Аккаунт жертвы становится привязанным к Google-идентичности злоумышленника.
Этот класс OAuth CSRF часто пропускают при тестировании, поскольку ревьюеры фокусируются на потоках логина и упускают потоки привязки/отвязки в настройках профиля.
Шаги воспроизведения (санитизированы)
Всё тестирование выполнялось только на аккаунтах, принадлежащих и контролируемых исследователем. Данные жертв не затрагивались.
Предусловия: Два тестовых аккаунта — аккаунт жертвы (victim@...), аутентифицированный в DomainScope, и контролируемый злоумышленником Google-аккаунт. Инструменты: браузер с прокси-перехватчиком.
Шаг 1 — Инициация авторизации Google злоумышленника:
- Войдите в DomainScope через LinkedIn (или основную идентичность), используя тестовую сессию злоумышленника.
- Перейдите в My Profile → Link Google Account.
- Завершите авторизацию Google с помощью контролируемого злоумышленником Google-аккаунта.
- Перехватите callback в DomainScope:
GET /connect/google?code=[REDACTED_AUTHORIZATION_CODE]&scope=email+profile&state=[REDACTED] HTTP/1.1 - Задокументируйте URL callback и отбросьте/задержите запрос. Извлеките значение
code. Обратите внимание: приложение не требовалоstate— удаление его всё равно приводило к успеху при повторе.
Шаг 2 — Создание CSRF PoC для жертвы:
- Создайте минимальную HTML-страницу, которая инициирует GET на callback с кодом злоумышленника и без валидного
state:<!-- csrf-poc.html — redacted, for authorized reproduction only --> <html> <body> <img src="https://domainscope.com/connect/google?code=[REDACTED_AUTHORIZATION_CODE]&scope=email+profile" style="display:none"> <!-- Alternative: auto-submit form or window.location --> <script>window.location = "https://domainscope.com/connect/google?code=[REDACTED_AUTHORIZATION_CODE]&scope=email+profile";</script> </body> </html> - Разместите эту страницу на подконтрольном злоумышленнику источнике (в тестах — локальный файл).
Шаг 3 — Взаимодействие жертвы и захват:
- Пока жертва аутентифицирована в DomainScope (активная сессия через
JSESSIONID/ session cookies), заставьте её посетить PoC URL. В реальном сценарии атаки это делается через фишинговую ссылку или встроенный ресурс. - Браузер жертвы отправляет аутентифицированный запрос на
/connect/googleс кодом злоумышленника. DomainScope привязывает Google-идентичность злоумышленника к профилю жертвы. - Затем злоумышленник заходит в DomainScope и выбирает Sign in with Google, аутентифицируясь через контролируемый им Google-аккаунт. Теперь злоумышленник аутентифицирован как жертва — полный захват аккаунта без пароля. Далее злоумышленник может установить новый пароль через My Profile без ввода текущего пароля (бизнес-логика для аккаунтов, созданных через LinkedIn), добиваясь устойчивого захвата.
Уязвимость проверялась с использованием двух аккаунтов, принадлежащих исследователю. Другие аккаунты не тестировались и не затрагивались.
Санитизированный PoC-запрос
Примечание: Чувствительные значения (коды авторизации, cookies, session ID) заменены на
[REDACTED]. Хост и поток сохранены для воспроизводимости. Для отчёта Bugcrowd было записано PoC-видео; публичный эксплойт не публикуется.
Исходный перехваченный callback через прокси (санитизирован):
GET /connect/google?code=[REDACTED_AUTHORIZATION_CODE]&scope=email+profile HTTP/1.1
Host: domainscope.com
User-Agent: Mozilla/5.0 [REDACTED]
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Referer: https://accounts.google.com/... [REDACTED]
Cookie: [REDACTED_SESSION_COOKIES]
Connection: closeДетали, подвергнутые санитизации:
code=4%2F...(длинный код авторизации) →code=[REDACTED_AUTHORIZATION_CODE]Cookie: s_fid=... s_evar... JSESSIONID=...(полная сессия) →Cookie: [REDACTED_SESSION_COOKIES]иJSESSIONID=[REDACTED]Referer: https://accounts.google.com/o/oauth2/...?client_id=...&xsrfsig=...→Referer: https://accounts.google.com/... [REDACTED]- значение
client_id→[REDACTED_CLIENT_ID] xsrfsigи другие длинные токены →[REDACTED]scope=email profile ...обобщено доemail+profileHost: domainscope.comсохранён — он идентифицирует Verisign DomainScope, который публичен.
Scope наблюдался как email profile (обобщённо). Дополнительные PII или цепочки токенов не включены.
Влияние — полный захват аккаунта через привязку Google OAuth
Подтверждённое влияние — полный захват аккаунта на уровне приложения:
- Обход аутентификации: После успешной CSRF-привязки злоумышленник аутентифицируется через доверенный поток Google OAuth. Приложение считает Google-идентичность владельцем аккаунта.
- Пароль не нужен: Пароль жертвы, если он вообще был, не требуется. Привязанный Google-аккаунт становится альтернативным путём аутентификации.
- Масштаб: Любой аккаунт DomainScope с активной сессией, посетивший подготовленную ссылку, мог быть привязан к Google-идентичности злоумышленника. Действие привязки изменяет состояние и не было защищено CSRF-токеном.
- Устойчивость: После привязки злоумышленник сохраняет доступ через вход Google до тех пор, пока привязка не будет вручную удалена жертвой или поддержкой. Жертва может не заметить дополнительную привязанную идентичность в настройках профиля.
Почему смена пароля не требовала текущего пароля — бизнес-логика: Основной логин DomainScope на тот момент был LinkedIn OAuth. После привязки функция смены пароля в My Profile не требовала текущего пароля — осознанное бизнес-решение для OAuth-аккаунтов, у которых пароль изначально не был установлен. После того как злоумышленник аутентифицировался через угнанную Google-привязку (как жертва), он мог перейти в My Profile → Change Password, установить новый пароль без указания текущего и далее сохранять доступ как через Google OAuth, так и через новый пароль — даже если жертва позже отвяжет Google-аккаунт.
Находка была зарепорчена как подтверждённый CSRF-до-захвата-аккаунта в потоке привязки OAuth. Терминология critical/zero-day в оригинальном отчёте не использовалась; проблема была триажирована как вознаграждаемая уязвимость.
Вознаграждение и таймлайн — $1,000 через Bugcrowd, Hall of Fame апрель 2018
- Программа: Bug bounty Verisign через Bugcrowd (только авторизованное тестирование).
- Подход к обнаружению: Найдено в ходе участия в программе Bugcrowd при анализе OAuth-потоков с ручным перехватом через прокси.
- Дата отчёта: Апрель 2018 (дата Hall of Fame согласно данным awards; совпадает с таймлайном раскрытия).
- Триаж и вознаграждение: Проверено, триажировано и вознаграждено $1,000 через Bugcrowd.
- Признание: Отмечено в Verisign Hall of Fame, апрель 2018.
- Профиль: Отчёты и признания перечислены в профиле Bugcrowd: https://bugcrowd.com/h/mahmoud_adel.

Дата исправления здесь не утверждается сверх публично подтверждённого. Отчёт был триажирован и вознаграждён; верификация патча, если она была, обрабатывалась через программу.
Митигация — проверка параметра OAuth state
Рекомендованные на тот момент митигации, в соответствии с лучшими практиками OAuth 2.0 (RFC 6749, раздел 10.12 — Cross-Site Request Forgery):
Требовать и проверять
stateна каждом OAuth callback:- Генерировать криптографически стойкое случайное значение (минимум 128 бит, например
crypto.randomBytes(32)). - Привязывать его к сессии пользователя на стороне сервера до редиректа к провайдеру.
- Проверять точное совпадение на callback
GET /connect/google. Отклонять запрос, еслиstateотсутствует, не совпадает или уже использован.
- Генерировать криптографически стойкое случайное значение (минимум 128 бит, например
Одноразовость и срок жизни:
- Инвалидировать
stateпосле первого использования. - Устанавливать короткий срок жизни (например, 10 минут) для ограничения окна повтора.
- Инвалидировать
Эшелонированная защита:
- Устанавить
SameSite=LaxилиStrictдля session cookies (JSESSIONIDи auth cookies), где совместимо. - Требовать повторную аутентификацию или дополнительный CSRF-токен для действий привязки аккаунта, а не только для логина.
- Валидировать
Referer/Originкак вторичную, а не основную проверку.
- Устанавить
Не полагаться только на секретность
code:- Код авторизации не является защитой от CSRF; он аутентифицирует Google-идентичность, а не пользователя, инициировавшего привязку.
Пример серверной проверки (псевдокод):
// При инициации привязки:
const state = crypto.randomBytes(32).toString('hex');
req.session.oauthState = { value: state, expiresAt: Date.now() + 10*60*1000 };
res.redirect(`https://accounts.google.com/o/oauth2/auth?...&state=${state}`);
// На callback /connect/google:
if (!req.query.state || req.query.state !== req.session.oauthState?.value) {
return res.status(403).send('Invalid OAuth state — possible CSRF');
}
if (Date.now() > req.session.oauthState.expiresAt) {
return res.status(403).send('OAuth state expired');
}
delete req.session.oauthState; // одноразовый
// затем обменять code и привязать аккаунт
При пентесте подобных потоков всегда тестируйте привязку, отвязку и повторную привязку, а не только основной OAuth-логин. Перехватите callback, удалите или замените state и оцените, выполняется ли действие всё ещё в сессии жертвы.
Выводы и уроки
- OAuth CSRF чаще всего пропускают именно в потоках привязки. CSRF логина получает внимание; CSRF привязки аккаунта — где внешняя идентичность прикрепляется к уже аутентифицированной сессии — часто остаётся незащищённым. Тестируйте каждый изменяющий состояние OAuth callback, включая эндпоинты
connect,linkиunlink. - Ручной перехват по-прежнему превосходит DAST для логических flaws. Автоматизированные сканеры подтвердили наличие OAuth, но не отметили отсутствие привязки
state. Перехват через прокси и ручное удалениеstateсразу подтвердили обход. Документируйте сырой callback, затем воспроизведите безstateпод второй сессией. - Параметр
state— это CSRF-токен, привязанный к сессии. Относитесь к нему соответственно: случайный, непредсказуемый, привязанный к инициирующей сессии, проверяемый точным сравнением и одноразовый. CookiesSameSiteпомогают, но не заменяют проверкуstate. - Изменяющие состояние GET усиливают CSRF. Callback использовал
GET. Любой GET, изменяющий привязку аккаунта, должен быть либо POST с CSRF-токеном, либо защищён проверкой OAuthstate. Где возможно, предпочитайте POST для завершения привязки. - Минимум SEO-шума, максимум сигнала. Эта находка использовала точные ключевые слова — CSRF, OAuth, State Parameter, Google OAuth, Verisign, DomainScope, Account Takeover, Bug Bounty, Responsible Disclosure — в отчёте, описании и заголовках. Чёткие шаги воспроизведения с санитизированными артефактами ускоряют триаж.
- Сообщайте только о проверенном. Программой на тот момент не присваивался CVSS; таймлайн патча не заявляется сверх триажа и вознаграждения. Сохранение таймлайна в пределах задокументированного (репорт апрель 2018, $1,000, HoF) сохраняет точность для будущего ревью.
Дисклеймер — ответственное раскрытие
Тестирование проводилось исключительно на аккаунтах, принадлежащих и контролируемых исследователем, в рамках авторизованной программы Verisign bug bounty на Bugcrowd. Данные клиентов, прод-аккаунты или несвязанные пользовательские сессии не затрагивались, не модифицировались и не тестировались. Всё воспроизведение использовало собственные тестовые идентичности LinkedIn и Google исследователя.
Этот материал опубликован после триажа и вознаграждения в рамках ответственного раскрытия, с заменой чувствительных значений на [REDACTED]. Имена хостов и детали потока сохранены для воспроизводимости и обучения защите. Активный эксплойт не предоставляется; PoC сведён к санитизированному GET и статичному HTML-шаблону.
По вопросам о находке или рекомендациям по митигации обращайтесь через профиль Bugcrowd https://bugcrowd.com/h/mahmoud_adel или страницу контактов сайта.