Как стать автором
Обновить

Атаки через новый OAuth flow, authorization code injection, и помогут ли HttpOnly, PKCE и BFF

Уровень сложностиСложный
Время на прочтение36 мин
Количество просмотров4K

В статье детально рассмотрим интересный вектор атаки на приложения, использующие OAuth/OIDC, разберем, какие предусловия для этого нужны, и увидим, что они не так недостижимы, как может показаться на первый взгляд. Узнаем, что такое «прерывание OAuth flow» и как его применять, затронем использование паттерна Backend‑for‑Frontend и способы реализации PKCE для confidential clients, попутно проверив, помогают ли они защититься от рассматриваемой атаки. Взглянем и на другие существующие рекомендации и предлагаемые лучшие практики, а также подумаем над прочими мерами защиты, которые действительно могут помочь. Все это с примерами, схемами и даже видео.

Материал будет интересен как для занимающихся разработкой приложений, так и для представляющих атакующую сторону.


Вся информация предоставлена исключительно в ознакомительных целях. Автор не несет ответственности за любой возможный вред, причиненный материалами этой статьи.


Оглавление

Введение

Про безопасность клиентских частей в контексте OAuth и OIDC можно говорить много. Обычно освещают вопросы безопасного хранения токенов на фронтенде, чтобы атакующий, имея возможность выполнения вредоносного JavaScript на страницах приложения, не мог получить к ним доступ.

Я и сам писал про это в начале 2023. С тех пор вопросы стали освещаться куда лучше, те же самые рекомендации были добавлены и в черновик OAuth 2.0 for Browser‑Based Applications. Поэтому сегодня будем говорить не про эту часть, а вместо этого предлагаю затронуть более интересную тему.

С одной стороны, как атакующий я могу попытаться украсть существующие токены из какого‑либо хранилища на клиентской части. Однако, если хорошо подумать, то это совсем не обязательно. Зачастую проще и эффективнее может быть вообще не обращать на них внимания, а получить свои собственные токены (спойлер: или даже не обязательно именно токены). Сегодня как раз поговорим про атаки, основанные на реализации нового, отдельного OAuth flow и не направленные на компрометацию уже существующих учетных данных (credentials) приложения. Атакующий попросту получает свои новые такие учетные данные.

Также стоит отметить популяризацию использования confidential clients, реализация которых в целом ложится на описываемые в документе OAuth 2.0 for Browser‑Based Applications паттерны, называемые Backend‑for‑Frontend (BFF) и Token Mediating‑Backend.

Паттерн Backend-for-Frontend (BFF)

Предлагаю различать обозначаемый здесь паттерн BFF с одноименным паттерном из мира распределенных систем и микросервисов во избежание путаницы. Если концептуально и тот и другой в целом можно считать client‑specific вариантом API Gateway, то по цели использования они уже отличаются. Обозначаемый в вышеупомянутом документе паттерн фокусируется именно на том, что BFF должен выполнять роль OAuth client, в то время как общие определения паттерна не включает это в набор обязательных характеристик и делают акцент на других аспектах.

Примечательно, что для обоих данных паттернов в блоках Mitigated Attack Scenarios упоминается о защите от атаки «Acquisition and Extraction of New Tokens». Вот как она определяется:

In this advanced attack scenario, the attacker completely disregards any tokens that the application has already obtained. Instead, the attacker takes advantage of the ability to run malicious code that is associated with the application's origin. With that ability, the attacker can inject a hidden iframe and launch a silent Authorization Code flow. This silent flow will reuse the user's existing session with the authorization server and result in the issuing of a new, independent set of tokens.

Не менее интересно и объяснение, как именно такая атака предотвращается:

The third scenario, where the attacker obtains a fresh set of tokens by running a silent flow, is mitigated by making the BFF/token-mediating backend a confidential client. Even when the attacker manages to obtain an authorization code, they are prevented from exchanging this code due to the lack of client credentials. Additionally, the use of PKCE prevents other attacks against the authorization code.

Запомним эти слова, к ним мы вернемся далее.

Использование confidential clients — это полезно. Однако хочу обратить внимание на то, что само по себе их применение не дает защиты от ряда атак, реализуемых при наличии у атакующего возможности внедрения вредоносного кода в страницы приложения.

Такие атаки тоже не новость и появились они далеко не вчера. Попробуем рассмотреть их подробнее на примерах.

Пример атаки

Говорить о безопасности в вакууме трудно, поэтому сначала обозначим краткую модель для нашего атакующего.

В рассматриваемых случаях атакующий есть внешний субъект (web attacker), имеющий возможность внедрения JavaScript‑кода в любую из страниц легитимного приложения, origin которой совпадает с origin адреса redirect URI.

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

Начнем с самого простого примера, в котором у нас есть легитимное приложение (client), которое реализует authorization code flow с использованием своей серверной части как confidential client. Обратил внимание, что порой паттерн BFF подается как более безопасный в сравнении с тем же token‑mediating backend за счет работы с токенами только на стороне серверной части, поэтому для наглядности наш confidential client в примерах будет реализован в BFF‑подобном стиле: с выпуском своего отдельного session ID и дальнейшим пропуском запросов к API через себя.

Схематично это можно изобразить так:

Небольшая видео‑демонстрация работы подобного приложения:

Видео на YouTube

Атака тогда может выглядеть следующим образом:

Видео:

Видео на YouTube (английские субтитры)

Как мы видим, наличие атрибута HttpOnly у cookie c session ID никак не мешает атакующему получить ее значение, потому что запрос он отправляет уже из своего подконтрольного окружения, и установка cookie (с любыми атрибутами) — это всего лишь заголовок ответа, который атакующий может прочитать.

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

Также стоит подчеркнуть, что рассматриваемые в статье сценарии атаки применимы и к паттерну token‑mediating backend, в этом случае атакующий сможет получить значение непосредственно самого access token для тех же целей.

Предусловия

Можно увидеть, что для успешного проведения такой атаки требуется соблюдения ряда условий.

1. Выполнение вредоносного JavaScript-кода в контексте одной из страниц приложения с тем же origin

Достигается оно в основном следующими способами:

  • Cross‑Site Scripting (XSS)

  • Скомпрометированной зависимостью или third‑party скриптом

Все мы знаем, что выделенная страница redirect URI дана нам для того, чтобы мы могли держать ее настолько пустой, насколько возможно, исключая наличие на ней какой‑либо функциональности и third‑party скриптов (даже аналитика может быть под вопросом). Это хорошо и правильно.

Нюанс в том, что для кражи authorization response уязвимость совершенно не обязательно должна присутствовать именно на этой самой странице. На самом деле, ограничение идет в рамках того же самого origin, поэтому на нем и делаю акцент. Полезно также отметить, что есть и механизмы, в которых на самом деле конкретная страница redirect URI не заложена by design, а сразу дается понять про доступность в пределах origin, это наглядно можно увидеть на примере следующего условия.

2. Возможность получения параметров authorization response

Как мы увидели в предыдущем пункте, в общем случае для кражи параметров authorization response атакующему может быть достаточно возможности выполнить свой JavaScript‑код на одной из страниц приложения. Здесь хочу кратко рассмотреть примеры основных способов реализации для такой атаки.

  • Использование web message response mode и установка своего обработчика событий

  • Вставка невидимого iframe и получение authorization response из location + прерывание OAuth flow

  • Кратковременное открытие нового окна и получение authorization response из location + прерывание OAuth flow

Среди различных response modes в OAuth есть и так называемый «web message response mode», предполагающий отправку параметров authorization response через postMessage API. Интересная особенность заключается в том, что он по сути не специфицирован. Есть только два просроченных черновика на раннем этапе проработки: OAuth 2.0 Web Message Response Mode и OAuth 2.0 Web Message Response Mode for Popup‑ and Iframe‑based Authorization Flows. Однако отсутствие общей спецификации не мешает данному подходу находить реальное применение. Например, можно рассмотреть описанную мной ранее реализацию аутентификации на портале разработчика Spotify, где как раз используется web message response mode.

В большинстве случаев реализация выглядит следующим образом: используется открытие в новом окне (popup) или же внутри iframe URL с адресом authorization request. В ответ на authorization request authorization server сам возвращает HTML‑страницу, которая средствами JavaScript отправляет параметры authorization response родительскому окну через postMessage.

Среди лучших практик в части безопасности для использования postMessage мы обычно встречаем реализацию проверки origin родительского окна (строгим сравнением со статическим значением) перед отправкой в него сообщения с параметрами authorization response. Мера, безусловно, важная: она не позволяет атакующему получить сообщение с authorization response на свой собственный origin.

Однако в контексте рассматриваемой атаки есть один нюанс: атакующий выполняет свой код на странице с тем же origin, что и другие страницы легитимного приложения. Соответственно, такая проверка origin никак не поможет для защиты от рассматриваемой атаки. Атакующий инициирует тем или иным способом выполнение нового authorization request, установит свой обработчик событий на получение событий «message» и успешно получит любезно отправленное в основное окно сообщение с параметрами authorization response.

Пример простого Proof‑of‑Concept (PoC) для реализации такой атаки:

window.addEventListener("message", (e) => {
    console.log(`Authorization response: ${JSON.stringify(e.data)}\nhas been stolen`);
});


function injectIframe(url) {
    let ifrm = document.createElement("iframe");
    ifrm.setAttribute("src", url);
    ifrm.style.width = "1px";
    ifrm.style.height = "1px";
    ifrm.style.display = "none";
    ifrm.setAttribute("id", 'test');
    document.body.appendChild(ifrm);
    return ifrm;
}


let url = ''; // URL of a legitimate authorization request
let ifrm = injectIframe(url);

Как уже становится понятно, основная суть заключается в вызове тем или иным способом authorization request в браузере пользователя с последующим доступом к параметрам authorization response. Действуя в рамках одного origin, у атакующего есть и другие возможности это сделать, например, с использованием объекта location у iframe или нового окна.

Как известно, в современных браузерах существует защитный механизм Same‑origin policy (SOP), который ограничивает взаимодействие страниц, имеющих различные origins.

Таким образом, если мы, например, находясь на странице https://alice.com, откроем новое окно с URL https://bob.com, мы не будем иметь доступа к его содержимому, включая объект location. Здесь на помощь приходит лежащая в основе подобных OAuth flow redirect‑based природа callback.

Так, для случая с iframe PoC может быть следующий:

function injectIframe(url) {
    let ifrm = document.createElement("iframe");
    ifrm.setAttribute("src", url);
    ifrm.style.width = "1px";
    ifrm.style.height = "1px";
    ifrm.style.display = "none";
    ifrm.setAttribute("id", 'test');
    document.body.appendChild(ifrm);
    return ifrm;
}


function stealAuthorizationResponse(frameObj, timerRef) {
    let hash = frameObj?.contentDocument?.location?.hash;
    if (hash.includes("code")) {
        frameObj.contentWindow.stop();
        clearInterval(timerRef);
        let code = hash.split('code=')[1];


        console.log(`Authorization code: ${code}\nhas been stolen`);
    }
}


let url = ''; // URL of a legitimate authorization request
let modifiedURL = url + '&response_mode=fragment&prompt=none';
let ifrm = injectIframe(modifiedURL);
let timer = setInterval(() => stealAuthorizationResponse(ifrm, timer), 1);


// fallback
setTimeout(() => {
    clearInterval(timer);
}, 5000);

А для случая с открытием нового окна следующий:

function stealAuthorizationResponse(windowObj, timerRef) {
    let hash = windowObj?.location?.hash
    if (hash.includes("code")) {
        windowObj.stop();
        clearInterval(timerRef);
        let code = hash.split('code=')[1];
        windowObj.close();


        console.log(`Authorization code: ${code}\nhas been stolen`);
    }
}


let url = ''; // URL of a legitimate authorization request
let modifiedURL = url + '&response_mode=fragment&prompt=none';
let newWindow = window.open(modifiedURL, 'targetWindow', 'popup, toolbar=no,location=no,status=no,menubar=no,scrollbars=no,resizable=no,width=1,height=1,left=0,screenY=100000, screenX=10000');
let timer = setInterval(() => stealAuthorizationResponse(newWindow, timer), 1);


// fallback
setTimeout(() => {
    clearInterval(timer);
}, 5000);

В примерах выше после выполнения редиректа на адрес redirect URI origin у страницы в окне/фрейме изменится на совпадающий с origin страницы родительского окна. Это и позволяет атакующему получить доступ к объекту location для извлечения из него параметров authorization response, поскольку такое поведение допустимо в рамках SOP.

Для случая с открытием нового окна требуется отметить определенный недостаток: пользователь все же на какое‑то короткое время может увидеть на экране созданное окно, пусть даже оно будет открыто в минимальном размере и в углу экрана, поскольку как раз‑таки в целях безопасности в современных браузерах для открываемых окон минимальный размер по ширине и высоте ограничен в 100 px.

Конечно, существуют и отличающиеся вариации JavaScript‑кода для подобных атак, однако их мы оставим для специализирующихся на этом людей. А у нас остаются еще две важные детали, без которых подходы с iframe/новым окном не будут до конца работать.

3. Прерывание OAuth flow

В приведенных выше примерах для iframe/нового окна существует одна особенность. В отличие от случая с web message, где на родительской странице, на которой выполняется код атакующего, может не быть штатного обработчика событий приложения или он может быть также переопределен, в этих двух случая параметры authorization response похищаются атакующим непосредственно с реального эндпоинта redirect URI.

О чем это говорит? В качестве одной из мер защиты от неправомерного использования authorization code очень широко применяется подход, делающий его одноразовым: так при попытке выполнения token request c ранее уже использованным authorization code большинство современных реализаций authorization server вернут ошибку, сообщающую о его некорректности.

Прервать OAuth flow означает предотвратить параметры authorization response (такие как authorization code) от их первичного получения и использования самим приложением. Это необходимо, чтобы authorization code еще не был использован на момент, когда атакующий сам выполнит его передачу приложению.

К счастью (для атакующего, конечно же), существует известная статья Account hijacking using “dirty dancing” in sign-in OAuth-flows от исследователя Frans Rosén, в которой автор разбирает подобные техники, называя это «the various ways to break the OAuth‑dance». Среди приведенных тактик присутствуют:

  • Передача невалидного значения state

  • Изменение response type/response mode для authorization request

  • Case‑shifting для значения redirect URI

  • Расширение пути (path) для значения redirect URI

  • Добавление query‑параметров к значению redirect URI

  • Использование когда‑то добавленных, но забытых дополнительных валидных значений для redirect URI

Также в ряде случаев для прерывания OAuth‑flow может быть использован вызов метода window.stop() для объекта окна со страницей redirect URI.

В рассматриваемых в данной статье примерах используется изменение response mode с query (по умолчанию) на fragment. Приложение изначально ожидает получить параметры authorization response в query‑части URL, однако указанием response_mode=fragment мы способствуем их передаче уже во fragment‑части URL, откуда приложение не рассчитывает на их получение.

4. Механизм SSO и прозрачный процесс авторизации пользователем

Также атака строится на принципе использования механизма Single sign‑on (SSO). У пользователя должна быть активная сессия на стороне authorization server, чтобы в ответ на authorization request не была возвращена форма аутентификации. У многих Identity Providers (IdP) такие сессии обычно имеют длительное время жизни для повышения удобства пользователей.

Важным является и обеспечение отсутствия какого‑либо взаимодействия в процессе предоставления авторизации, чтобы сделать его неявным для пользователя, если ранее он уже предоставлял приложению согласие (consent). Некоторые реализации authorization server изначально делают экран согласия (consent screen) обязательным, однако оставляют возможность для передачи параметров в authorization request, сообщающих о необходимости его пропуска. Это может быть параметр prompt=none или иные неспецифицированные аналоги вроде «force_confirm» или «force_authn».

Итак, здесь мы рассмотрели необходимые для проведения успешной атаки предусловия. На первый взгляд они могут показаться множественным и труднодостижимыми, однако не позволяйте этой мысли расслабить вас. В большинстве случаев наличие возможности исполнения вредоносного кода (первое условия) автоматически обеспечит вам выполнение второго. А многообразие имплементаций и подходов позволяет во многих случаях найти доступный вариант и для прерывания OAuth flow, и для прозрачного прохождения процесса авторизации.

BFF не панацея

Как уже было сказано ранее, автору виден тренд в продвижении подходов с использованием confidential clients (а особенно BFF) в качестве некоей серебрянной пули как лучшего из имеющихся на текущий момент подходов, дающего защиту от большинства атак.

Например, такую мысль можно уловить и в достаточно популярном выступлении The insecurity of OAuth 2.0 in frontends от Philippe de Ryck. К сожалению, текстовый вариант отсутствует, поэтому вынужден отсылаться к видео. В первой части выступления автор поднимает действительно важные вопросы безопасности, в том числе упоминая и атаки через новый OAuth flow, однако с дальнейшим решением, заключающимся согласно видео в использовании BFF, согласиться уже трудно. На отрезке 38:22 — 47:18 можно послушать приведенные аргументы. Автор буквально говорит про использование BFF: «That’s as good as it gets. We can’t do better» (46:40).

Однако из рассмотренного нами выше примера мы уже наглядно видим, само по себе использование BFF не дает особого выигрыша в сравнении с прочими подходами при наличии у атакующего возможности инъекции JavaScript‑кода на одной из страниц приложения.

Поговорим немного и про сам подход. В нем BFF играет роль confidential client, выполняет всю работу с токенами на стороне серверной части (backend), а клиентской части (frontend) передает, например, отдельный session ID уже в HttpOnly‑cookie. Подход в целом несомненно полезен, использование confidential client — разумеется, благое дело, однако сам по себе он не защищает от описанной атаки. Еще раз посмотрим на цитату из приведенного в OAuth 2.0 for Browser‑Based Applications описания атаки Acquisition and Extraction of New Token, которую BFF должен предотвращать:

With that ability, the attacker can inject a hidden iframe and launch a silent Authorization Code flow. This silent flow will reuse the user's existing session with the authorization server and result in the issuing of a new, independent set of tokens.

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

Да, мы можем не передавать access token клиентской части. Но в данном случае мы всего лишь изобрели новый уровень абстракции, чтобы решить проблемы: этот токен атакующему уже и не так нужен. У нас теперь другой публичный интерфейс, который принимает session ID для аутентификации запросов, но все необходимые приложению API все равно будут доступны через него.

В рекламе такого подхода порой происходит некоторая подмена понятий, где он сравнивается с использованием public clients, то есть приложений имеющих только клиентскую часть, и сервисов, которые выставлены своими API наружу напрямую. В реальной жизни для приложений, для которых нефункциональные требования (НФТ) в части информационной безопасности и надежности имеют хоть какое‑то значение, у приложения будет и серверная часть, и сами сервисы будут скорее всего выставляться через нечто, реализующее паттерн API Gateway (вариантов реализации которого несколько).

Также и access token, получаемый приложением, опять же в рамках ортогональной активности может (а по‑хорошему, и должен) быть ограничен через audiences и/или scopes и тоже не подойдет для доступа к «любым API».

В общем, здесь может наблюдаться смешение в кучу разных независимых вопросов, чем и смущают честной народ. Грамотная работа с API сервисов — это один вопрос, а реализация для работы с OAuth/OIDC на стороне приложения — совершенно другой.

Но не будет слишком уходить в критику подобного подхода, чтобы у читающего не сложилось впечатление о том, что его использовать не нужно. Еще раз хочу повторить, что сами подходы использующие confidential clients, к которым относится и BFF, в любом случае имеют преимущества с точки зрения безопасности над подходами с public clients. Однако не нужно слепо им следовать: всех рисков они тоже не снимают, и поэтому понимать их для каждого приложения очень важно.

Внимательный читатель здесь скажет, что мы немного слукавили. Когда мы цитировали объяснение, почему BFF/token‑mediating backend предотвращают атаку Acquisition and Extraction of New Token, там было еще последнее предложение:

Additionally, the use of PKCE prevents other attacks against the authorization code.

Так может быть, все проблемы у нас от того, что мы не использовали PKCE?

Поможет ли PKCE

PKCE (помним, что читается как «pixie») или же RFC 7636 Proof Key for Code Exchange by OAuth Public Clients существует уже довольно давно. PKCE в своей сути делает одну вещь: позволяет убедиться, что сторона (party), завершающая authorization code flow, и есть сторона, начавшая его.

PKCE задумывался как защита от использования похищенного authorization code для получения токенов у authorization server (чего мы для confidential clients, естественно, сделать в принципе не сможем).

A unique code verifier is created for every authorization request, and its transformed value, called "code challenge", is sent to the authorization server to obtain the authorization code. The authorization code obtained is then sent to the token endpoint with the "code verifier", and the server compares it with the previously received request code so that it can perform the proof of possession of the "code verifier" by the client.

В RFC акцент делается на проверке соответствия на стороне authorization server:

An attacker who intercepts the authorization code at (B) is unable to redeem it for an access token, as they are not in possession of the "code_verifier" secret.

Изначально придуманный для использования public clients, подход далее стал широко применяться и для confidential clients. Однако рекомендаций по реализации для такого случая в самом RFC мы ожидаемо не найдем: говорится только о том, что code verifier и code challenge генерирует client. При этом надо помнить, что client — понятие довольно абстрактное. Client может включать и серверную, и клиентскую части, а взаимодействие между ними при этом может происходить множеством разных способов.

Таким образом, сперва у нас встает вопрос: а как в принципе реализовать PKCE для случая c confidential clients?

PKCE для confidential clients

Если мы посмотрим, как реализуется PKCE в окружающих нас приложениях, а также на существующие в интернете способы и рекомендации, то сможем выделить два принципиально различающихся подхода.

Генерация code verifier и code challenge на стороне клиентской части

Первый подход заключается в генерации значения code verifier и извлечении на его основе значения code challenge непосредственно на клиентской части силами JavaScript, благо все возможности для этого есть.

Сначала приложение перед формированием параметров для authorization request генерирует случайную строку достаточной длины и энтропии, а затем, например, с помощью window.crypto.subtle.digest() получает SHA-256 digest от нее (на самом деле не напрямую от нее, а от массива байтов). И далее полученный digest приводится к формату base64 for URL (base64url).

Логично, что серверная часть приложения в таком случае ничего про сгенерированный code verifier не знает, и его нужно ей явно передать. Для этого нужно сначала сохранить некое состояние между двумя открытыми страницами: страницей, где формировались параметры authorization request и страницей redirect URI. Для этого неплохо подходит использование sessionStorage, если все переходы осуществляются в пределах одного окна (вкладки). В ряде реализаций разработчики также используют localStorage.

Далее со стороны клиентской части на странице redirect URI нам необходимо извлечь сохраненное ранее значение code verifier, чтобы передать его бэкенду. Логично, что при этом сам callback‑эндпоинт (route) redirect URI не сможет сам извлечь такое значение, поэтому в данном случае серверу приложения необходимо сначала вернуть в качестве страницы redirect URI HTML‑страницу, JavaScript‑код на которой уже сможет получить значение authorization code из объекта location, значение code verifier из sessionStorage и отправить отдельный запрос своей серверной части с их передачей, в ответ на который уже будет установлена cookie с session ID.

Тогда полученная реализация будет выглядеть подобным образом:

При этом вполне логично заметить, что атакующий в нашей модели имеет возможность аналогичным образом сгенерировать свои собственные значения code verifier и code challenge (или даже использовать заранее заготовленные) и далее так же передать подходящее значение code verifier вместе с authorization code:

Видео на YouTube (английские субтитры)

Кстати, подход с прерыванием OAuth flow через вызов метода window.stop(), про который упоминали ранее, применим как раз к подобным реализациям: поскольку здесь основной запрос отправляется уже после загрузки изначальной страницы, таким образом можно предотвратить его выполнение.

Автор также встречал вариант реализации данного подхода, в котором code verifier и code challenge генерируются на стороне серверной части, но оба в открытом виде передаются клиентской части. Очевидно, что данный способ принципиальных отличий не имеет и дополнительных мер безопасности в данном случае не предоставляет.

Таким образом, мы видим, что данный подход пока что не позволяет нам защититься от рассматриваемой атаки.

Генерация code verifier и code challenge на стороне серверной части с передачей клиентской только code challenge

В следующем подходе за генерацию code verifier и code challenge будет отвечать уже серверная часть приложения. При этом само значение code verifier не будет раскрываться клиентской части вовсе.

Итак, сначала аналогичным приведенному ранее способу серверная часть производит генерацию code verifier и извлечение на его основе code challenge. Также для чистоты эксперимента здесь мы добавим и использование state, обеспечивающее защиту от CSRF для приложения, а также использование nonce из OIDC.

Клиентской части же будут возвращены только значения, требующиеся для использования в authorization request: code challenge, state, nonce.

Однако на стороне серверной части нам для выполнения token request также нужно каким‑либо образом обеспечить возможность найти значение code verifier, соответствующее которому значение code challenge было использовано в authorization request.

Для этого, можно, например, поступить следующим образом. Приложение случайным образом генерирует code verifier и получает на его основе code challenge. Также случайным образом генерируются значения state и nonce. Далее в соответствие значениям code verifier, state и nonce приложение генерирует значение, которое назовем pre‑auth session, и сохраняет полученные данные в структуре вида:

Тогда в ответ на запрос параметров для authorization request со стороны клиентской части бэкенд возвращает значения code challenge, state и nonce (или подготовленный URL целиком) в теле ответа, а значение pre‑auth session в устанавливаемой HttpOnly‑cookie (с грамотным указанием всех прочих атрибутов). А при обработке запроса к redirect URI‑эндпоинту приложение получает не только значения code и state из параметров запроса, но и значение pre‑auth session из отправленной браузером cookie.

Получив такой запрос, приложение извлекает значение pre‑auth session и проверяет, существует ли оно в записанном хранилище. Далее также выполняется сверка переданного значения state со значением, сохраненным в соответствие для pre‑auth session. И уже если все проверки успешны, приложение получает соответствующее значение code verifier, с которым и выполняет token request.
Nonce, как известно, используется уже после получения token response для сверки со значением одноименного claim в ID token.

Логично, что из браузера пользователя атакующий не сможет в общем случае получить значение HttpOnly‑cookie с помощью JavaScript, на чем подход и строится.

Приведенный пример описывал stateful‑реализацию, может использовать также и stateless вариант.
В нем после генерации значений code verifier, code challenge, state и nonce приложение не записывает их в какое‑либо хранилище, а шифрует:
pre_auth_session = encrypt(code_verifier, state, nonce). Тогда, получив значение cookie в запросе к redirect URI, приложение может выполнить дешифрование и извлечь полученные значения. Такой подход, кстати, и используется в демо‑приложении, примеры с которым вы можете наблюдать в приложенных видео.

Стоит отметить, что здесь постарался привести наиболее простые способы реализации. При сохранении общей идеи в реальной жизни можно встретить и другие вариации. Так, например, достаточно полезно может быть иметь некоторый Time‑to‑Live (TTL) для подобной записи, по истечении которого использование значений уже будет невозможно.

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

Позволит ли данная схема защититься от обсуждаемой атаки? Как мы видим, атакующий действительно не сможет получить нужное значение pre‑auth session из браузера пользователя. Подвох в том, что оно ему и не нужно. Атакующий сначала может выполнить запрос на получение параметров authorization request из своего окружения, получив в ответе как значения code challenge, state и nonce, так и соответствующее им значение pre‑auth session в устанавливаемой сервером cookie.
Как мы же писали ранее, установка cookie в данном случае — всего лишь заголовок ответа, который, естественно, будет доступен атакующему. А далее атакующий выполняет вызов authorization request с полученным им самим параметрами уже в браузере пользователя, получает authorization response и передает его из своего окружения в запросе к redirect URI приложения вместе с ранее полученным значением cookie.

Видео на YouTube (английские субтитры)

Также для наглядности покажем, что атаку можно выполнить и «вручную»:

Видео на YouTube (английские субтитры)

Таким образом, в данном примере мы собрали все предлагаемые нам лучшие практики:

  • authorization code grant flow (не предполагает передачи токенов в authorization response)

  • state (защита от CSRF со стороны client)

  • nonce (защита от replay attacks)

  • code verifier + code challenge (защита от CSRF со стороны authorization server)

  • confidential client (предотвращает возможность получения токенов у authorization server с помощью похищенного authorization code)

  • Backend‑for‑Frontend pattern (предотвращает раскрытие полученных токенов клиентской части)

  • HttpOnly атрибут у cookie (предотвращает доступ к значению cookie из JavaScript, выполняемого на клиентской части)

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

Рекомендации из BCP for OAuth 2.0 Security

Как мы сказали выше, такая атака известна уже в течение некоторого времени. В общем случае она носит название authorization code injection attack, а рассматриваемый в статье сценарий можно назвать частным ее случаем.

Так, например, документ RFC 9700 Best Current Practice for OAuth 2.0 Security, не так давно получивший статус RFC после многолетнего нахождения в черновике, упоминает и данную атаку в том числе.

П. 2.1.1 говорит нам:

Clients MUST prevent authorization code injection attacks (see Section 4.5) and misuse of authorization codes using one of the following options:

  • Public clients MUST use PKCE [RFC7636] to this end, as motivated in Section 4.5.3.1.

  • For confidential clients, the use of PKCE [RFC7636] is RECOMMENDED, as it provides strong protection against misuse and injection of authorization codes as described in Section 4.5.3.1. Also, as a side effect, it prevents CSRF even in the presence of strong attackers as described in Section 4.7.1.

  • With additional precautions, described in Section 4.5.3.2, confidential OpenID Connect [OpenID.Core] clients MAY use the nonce parameter and the respective Claim in the ID Token instead.

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

This document therefore recommends instead binding every authorization code to a certain client instance on a certain device (or in a certain user agent) in the context of a certain transaction using one of the mechanisms described next.

А далее как раз описаны механизмы, из которых можно выбрать подходящий:

  1. Использование PKCE

  2. Использование nonce из OIDC

PKCE is the most obvious solution for OAuth clients, as it is available at the time of writing, while nonce is appropriate for OpenID Connect clients.

Однако тут же справедливо указываются и ограничения:

An attacker can circumvent the countermeasures described above if they can modify the nonce or code_challenge values that are used in the victim's authorization request. The attacker can modify these values to be the same ones as those chosen by the client in their own session in Step 2 of the attack above. (This requires that the victim's session with the client begins after the attacker started their session with the client.) If the attacker is then able to capture the authorization code from the victim, the attacker will be able to inject the stolen code in Step 3 even if PKCE or nonce are used.

Упоминание атаки в FAPI 2.0 Security Profile

Сам акроним FAPI изначально означал Financial‑grade API, однако далее было решено, что спецификация применима не только к financial services, но и к другим случаям, где повышенная безопасность важна.

FAPI 2.0 Security Profile есть профиль, то есть набор мер, для обеспечения безопасности API, основанный на OAuth 2.0 и применимый для защиты API, имеющих повышенную ценность и особые требования в части информационной безопасности.

П. 5.6.7 данного документа упоминает и рассматриваемую нами атаку, однако она уже носит название «Browser‑Swapping Attack».
Приведенное в нем описание атаки несколько отличается и, на мой взгляд, не так явно раскрывает проблему.
Описываемый сценарий чуть другой и более напоминает фишинг: он предполагает, что атакующий вводит в заблуждение пользователя, и пользователь добровольно авторизует доступ:

The victim may be tricked into believing that an authentication/authorization is legitimately required. The victim therefore authenticates at the authorization server and may grant the client access to their data

К тому же атакующему совсем не обязательно использовать непосредственно браузер (это мы как раз видели в рассмотренных примерах), что тоже может вносить путаницу ввиду названия атаки.

Спецификация отмечает:

With currently deployed technology, there is no way to completely prevent this attack if the authorization response leaks to an attacker in any redirect-based protocol. It is therefore important to keep the authorization response confidential. The requirements in this security profile are designed to achieve that, e.g., by disallowing open redirectors and requiring that the redirect_uri is sent via an authenticated and encrypted channel, the pushed authorization request, ensuring that the redirect_uri cannot be manipulated by the attacker.
Implementers need to consider the confidentiality of the authorization response critical when designing their systems, in particular when this security profile is used in other contexts, e.g., mobile applications.”

Таким образом, говорится о том, что полноценная защита (для redirect‑based случаев, это важно) невозможна, и данный профиль описанными в нем мерами стремится к повышению конфиденциальности authorization response. Здесь важно понимать, что, во‑первых, применение FAPI 2.0 profile не защищает полностью, а во вторых, про защиту от данной атаки полезно помнить и тем, кто не реализует у себя в обязательном порядке все меры из этого профиля.

Актуальность и применимость атаки

Интересно, что RFC 9700 Best Current Practice for OAuth 2.0 Security в приведенной модели не имеет атакующего с рассматриваемыми нами возможностями, нельзя его и «собрать» путем комбинации нескольких пунктов. Что как раз может быть связано с рассмотренными ограничениями для атаки authorization code injection.

Отсутствует подобный атакующий и в FAPI 2.0 Attacker Model, где так и говорится:

Note: An attacker that can read the authorization response is not considered here, as, with current browser technology, such an attacker can undermine most security protocols. This is discussed in "Browser Swapping Attacks" in the Security Considerations in the FAPI 2.0 Security Profile.

Примечательно, что в предыдущих версиях документа в нем присутствовал атакующий с похожими возможностями (A3b):

The capabilities of the web attacker, but can also read the authorization response. This can happen e.g., due to the URL leaking in proxy logs, web browser logs, web browser history, or on mobile operating systems.

Однако далее этот пункт был исключен в виду того, что наличие такой модели делало атакующего слишком «могущественным», что приводило к сложностями для защиты от всех возможных атак. К тому же модель атакующего достаточно бинарна: или атакующий включен в нее (и тогда меры по защите должны предотвращать все атаки, возможны для такого атакующего) или не включен (тогда уже никаких гарантий для подобного атакующего не предоставляется). Нюансы и тонкости трудно отразить в модели атакующего, поэтому и было принято решение об исключении A3b, чтобы явно указать, что подобный атакующий в документе не рассматривается. Здесь хочу выразить благодарность Daniel Fett, автору FAPI 2.0 Attacker Model, за сообщение подробностей мотивации исключения данного атакующего.

Все это может натолкнуть на мысль о том, что не просто так подобный атакующий исключен из моделей. Может быть, действительно, мы пытаемся показать атаку более грозной и распространенной, чем она есть на самом деле?

К сожалению, к такому выводу прийти сложно. Атакующий, имеющий возможность выполнения JavaScript‑кода на одной из страниц легитимного приложения, встречается в реальной практике не так редко. В частности, как мы сказали, любая XSS (которая не перестает быть менее распространенной) дает ему подобную возможность. Можно посмотреть на статьи, где атакующие используют XSS в сочетании с атаками на OAuth, из недавних известных привожу в пример:

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

Кроме того, существует довольно популярная позиция о том, что «XSS is game over», то есть само наличие XSS означает наличие у атакующего таких возможностей, от которых уже не защититься. XSS — действительно достаточно значимая уязвимость, однако хочу обратить внимание, что воздействие (impact) от ее эксплуатации также имеет значение. Если в ходе эксплуатации XSS атакующий может получить дополнительное воздействие, приводящее к account takeover, такая уязвимость может быть оценена как более критичная, поскольку имеет бОльшие последствия.

Таким образом, атака все еще актуальна, и даже если мы будем усердно пытаться не обращать на нее внимания, она от этого не исчезнет.

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

Меры защиты

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

Сам authorization code grant в OAuth по своим принципам, by design, закладывает разбиение процесса авторизации на отрезки, при этом позволяя выполнить отдельный участок процесса вообще вне сеанса пользователя, чем нивелируется наличие любых защитных механизмов браузера. Убедимся в этом на нашем последнем примере:

Как видно из приведенной диаграммы, у нас отсутствует возможность обеспечить явную связь между шагами [2] и [3], и [5] и [6]. Мы не можем гарантировать, что code challenge c0, state s0, nonce n0, передаваемые на шаге [3], и есть «правильные» значения, полученные на шаге [2].

Аналогично не можем мы обеспечить и передачу в шаге [6] тех же самых значений, что были получены на шаге [5]. Ключевой шаг [6], который как раз приводит к дальнейшему получению токенов, и вовсе основан на данных, получаемых с недоверенной клиентской стороны.

Единственное условие, которое должно выполняться в подобной схеме, это соответствие значений между шагами [2] и [6].

Таким образом принципиальных мер защиты для подобной схемы я действительно увидеть не могу. Все дополнительные меры вроде применения PAR/JAR/JARM также не предотвращают подобную атаку.

Поэтому в качестве основной меры защиты можно использовать способы, делающие невозможным один из обязательных шагов атаки. Так, в целом атака будет невозможной, если атакующий не сможет получить доступ к authorization response в браузере пользователя. Как мы уже убедились, использование таких response modes как query, fragment и web_message оставляют authorization response доступным атакующему, выполняющему JavaScript‑код на том же самом origin. Однако есть еще один response mode, который в данном случае имеет принципиальное отличие.

Form Post Response Mode

OAuth 2.0 Form Post Response Mode, определенный в одноименной спецификации, не предусматривает доступности authorization response с origin самого приложения.

В данном случае authorization server в ответ на authorization request возвращает HTML‑страницу с формой, которая при загрузке страницы через user agent автоматически отправляет параметры authorization response на адрес redirect URI (<form method="post" action="https://site.com/callback">). Таким образом данные передаются через POST‑запрос к обозначенному эндпоинту. Подход на самом деле не новый, аналогичное мы могли наблюдать еще со времен SAML POST Binding.

Ключевой деталью здесь является то, что параметры authorization response содержит страница, находящаяся не на origin самого приложения, а на origin authorization server. При этом данные параметры передаются не клиентской части приложения, как в случае с web message, а уже серверной, что также исключает ряд возможностей для манипуляции.

Видео на YouTube

Так при использовании подобного response mode атакующий с рассматриваемыми нами возможностями не будет способен получить доступ к authorization response в браузере пользователя.

Интересной деталью является и факт, что в данном случае cookie со значением pre‑auth session необходимо устанавливать уже с атрибутом SameSite = None. Поскольку нам требуется передача значения этой cookie в POST‑запросе к серверной части, то же де‑факто стандартное значение Lax не позволит выполнить ее передачу, так как такой запрос не является top‑level navigation. Подробнее можно прочесть в упоминаниях от столкнувшихся с этим компаний: [1], [2], [3].

При этом очень важно отметить, что для того чтобы данная мера возымела эффект, на стороне authorization server обязательно должна присутствовать возможность ограничения доступных response modes на каком либо из уровней: client/realm/authorization server. Если этого не сделать, атакующий все так же может изменить response mode путем манипуляции параметрами authorization request, что сведет на нет все усилия.

Также при использовании form post response mode следует обратить особое внимание на то, какие значения redirect URI authorization server считает валидными. Так, например, при возможности указать в качестве redirect URI значение с URI‑схемой (или же псевдопротоколом) javascript: возможна атака уже на сам authorization server, дающая возможность получения XSS уже на его стороне, что особенно важно при поддержке динамической регистрации клиентов (DCR). Подробнее можно прочесть в статье POST to XSS: Leveraging Pseudo Protocols to Gain JavaScript Evaluation in SSO Flows.

Дополнительные меры защиты

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

Защита от внедрения вредоносного JavaScript

Безусловно, сама защита от возможности выполнения вредоносного JavaScript в приложении остается важной и необходимой. Даже при невозможности проведения подобной атаки, выполняя код на странице приложения в браузере пользователя, атакующий все еще сможет отправлять любые запросы к вашим API, пока такая страница открыта. Меры по повышению безопасности при использовании внешних зависимостей и защите от XSS освещаются довольно широко, поэтому их оставим за рамками данной статьи.

Защита от возможности использования iframe

Полезно может быть ограничение возможности атакующего загрузить iframe c URL authorization request, чтобы атака уже не была полностью незаметной для пользователя.

Для этого на стороне authorization server важно использовать CSP‑директиву frame‑ancestors, и в дополнение на стороне приложения — директиву frame‑src.

Естественно, если вашему приложению важен так называемый «silent flow», это ограничит возможность и его применения. Также на возможность проведения атаки с открытием нового окна здесь мы повлиять не можем.

Ограничения для невозможности сделать процесс авторизации пользователем полностью прозрачным и незаметным

Одной из возможных мер, затрудняющих проведение атаки, может быть реализация обязательного взаимодействия с пользователем в процессе grant flow. Например, потребуется физическое нажатие на кнопку «Подтверждаю» каждый раз. Тогда при должной защите от clickjacking незаметно для пользователя этот шаг уже не пройдет.

Но, конечно, при этом мы получаем некоторое негативное влияние на UX: у пользователя появляется необходимости в дополнительном действии, клике. Об этом кстати, говорится и в п. 5.1.3 OAuth 2.0 for Browser‑Based Applications.

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

Ограничение использования web message response mode

PostMessage для меня вообще является некоторой аналогией загрузки файлов, но из мира фронтенда: если можно обойтись без этого, то лучше так и сделать. Иначе получается много мест, где можно обжечься.

С одной стороны использование web message позволяет многим приложениям реализовывать удобный для пользователя «silent flow». С другой же, если такой response mode поддержан, непосредственных мер для защиты от утечки authorization response я не вижу: все ограничения будут только на уровне origin. Поэтому для приложений с повышенными требованиями ИБ имеет смысл обеспечить невозможность его использования, включая ограничение на доступные response modes на стороне authorization server.

Привязка authorization code к IP/fingerprint

Еще одним направлением защиты может являться ограничение возможности атакующего использовать похищенные параметры authorization response уже из своего, подконтрольного окружения.

Так, к примеру, при создании authorization code можно привязывать его к отпечатку (fingerprint) конкретного устройства или же к IP‑адресу, чтобы далее при его передаче приложению можно было выполнить сверку.

Естественно, это всего лишь полумеры: так подделка fingerprint все еще будет возможна, а привязка к IP не защитит при атаках, где атакующий будет действовать из той же сети, что и пользователь. Однако в зависимости от конкретной модели угроз такие подходы могут усложнить успешное проведение атаки.

Перенос redirect URI на другой origin

Также порой можно рассмотреть вынос страницы redirect URI на другой, отличный от основной части приложения origin, чтобы атакующий не мог таким образом добраться до параметров authorization response. Например, приложение у нас находится на https://app.site.com. Тогда мы можем использовать для страницы redirect URI origin https://callback.app.site.com, причем на данном поддомене ничего, кроме самого redirect URI, располагаться не должно. Если полагать, что на callback.app.site.com у нас никаких уязвимостей нет, то даже выполняя JavaScript в контексте страницы на app.site.com, атакующий не сможет получить доступ к объекту location для окна с другим origin.

Однако в таком подходе есть и существенный недостаток: c домена callback.app.site.com мы можем установить cookie для того же самого домена (wildcard или host‑only) или для вышележащих доменов (только wildcard). Таким образом, устанавливая cookie для app.site.com мы на самом деле сделаем ее доступной и всем поддоменам app.site.com, что обычно нежелательно, поскольку открывает возможности для манипуляции с cookies в случае наличия уязвимости на каком‑либо поддомене или атаки subdomain takeover. Поэтому для данного подхода с точки зрения защиты вижу довольно ограниченную применимость только в случаях, когда использование wildcard‑cookie намеренно и оправдано.

Аудит возможностей authorization server с точки зрения возможностей для прерывания OAuth flow

Последним по списку, но не по значимости, будет понимание актуальных для вас рисков и работа с ними. Зная актуальные современные векторы атак, полезно анализировать работу конкретной реализации authorization server на предмет устойчивости к ним. Снижение перечня применимых тактик для прерывания OAuth flow для вашего authorization server также может повлиять на успех атаки.

Кроме этого вижу возможным применение практик, направленных уже не на предотвращение непосредственно, а на выявление подозрения на компрометацию и применение ответных мер, например, continuous authentication.

Примеры из истории

Похожую атаку я также встретил на слайде 10 в материалах ко встрече‑обсуждению draft‑ietf‑oauth‑security‑topics-13, проходившей в 2019 году. Исследователь Daniel Fett отображал атаку с аналогичным сценарием (кстати, именно из его слайдов я позаимствовал идею с наименованием параметров вида s0, cc0 и т. д.)

В качестве решения для защиты была предложена концепция IVAR: Integrity Verification for Authorization Requests.

Однако идея существовала только в виде черновика одной ревизии и дальнейшего развития не получила.

Заключение

В статье рассмотрели вариацию атаки authorization code injection и сравнили защиту от нее, предоставляемую предлагаемыми в индустрии лучшими практиками. Да, мы используем authorization code grant flow, добавляем confidential client, BFF, применяем PKCE, state, nonce и HttpOnly‑cookies — столько умных слов, что от одного их перечисления уже можно почувствовать себя защищенным. Но это может оказаться ложным чувством. Используйте все перечисленные практики, они полезны для для защиты от тех атак, на которые направлены. Но, применяя их, старайтесь понять, для чего они нужны.

К сожалению, очень многие практики из OAuth и по сей день используются разработчиками без сильного погружения в детали, принимаются как догмат: слепо, на веру. Однако бывает полезно заглянуть внутрь них и понять, как именно и от чего та или иная практика на деле должна (или не должна) защищать.

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

Это применимо и к материалу, который вы только что прочли: не верьте на слово, сядьте, подумайте, помоделируйте. Никто не безупречен, вполне возможно, что найдете ошибки и слабые места и в моей логике, можете потом о них написать, буду рад обсудить.
А чтобы моделирование было проще, исходный код демо‑приложения, которое использовалось в приложенных видео, доступен на GitHub, поэтому все описанное можете повторить и проверить самостоятельно.

Больше материалов про аутентификацию и Identity & Access Management можно найти в моем телеграм‑канале: «401 Unauthorized» @unauthz.

Статья также доступна на английском языке.

Теги:
Хабы:
+16
Комментарии3

Публикации

Истории

Работа

Ближайшие события

19 марта – 28 апреля
Экспедиция «Рэйдикс»
Нижний НовгородЕкатеринбургНовосибирскВладивостокИжевскКазаньТюменьУфаИркутскЧелябинскСамараХабаровскКрасноярскОмск
24 апреля
VK Go Meetup 2025
Санкт-ПетербургОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань
14 мая
LinkMeetup
Москва
5 июня
Конференция TechRec AI&HR 2025
МоскваОнлайн
20 – 22 июня
Летняя айти-тусовка Summer Merge
Ульяновская область