[Из песочницы] CSRF-уязвимость VK Open Api, позволяющая злоумышленнику без ведома пользователя получать Access Token’ы сторонних сайтов, на которых реализована авторизация через VK
Представляю вашему вниманию обзор уязвимости, связанной с неправильным применением JSONP в VK Open Api. На мой взгляд, уязвимость достаточно серьёзная, т.к. позволяла сайту злоумышленника получать Access Token другого сайта, если на нём используется авторизация через библиотеку VK Open API. На данный момент уязвимый код поправили, репорт на HackerOne закрыли, вознаграждение выплатили (1,500$).
Как это выглядело
В принципе, процесс получения пользовательского Access Token’а страницей злоумышленника происходил по стандартной схеме эксплуатации CSRF-уязвимости:
- Пользователь заходит на сайт, использующий библиотеку VK Open API (например, www.another-test-domain.com).
- Авторизуется там через VK.
- Потом заходит на сайт злоумышленника (например, www.vk-test-auth.com), который, эксплуатируя уязвимость, получает Access Token, принадлежащий сайту www.another-test-domain.com.
- Получив Access Token пользователя, злоумышленник может обращаться к VK API с теми правами, который пользователь дал сайту www.another-test-domain.com при авторизации на нем через VK.
Демонстрация
На видео показано, как страница «злоумышленника» на домене www.vk-test-auth.com получает Access Token пользователя VK, который авторизовался на сайте www.another-test-domain.com, несмотря на то, что в настройках приложения VK, доступ разрешён только для домена www.another-test-domain.com.
Конечно, домены я не регистрировал, т.к. в данном случае это не играет никакой роли. Когда записывался скринкаст, они были прописаны в hosts.
Немного о VK Open API
Выдержка из официальной документации:
Open API — система для разработчиков сторонних сайтов, которая предоставляет возможность легко авторизовывать пользователей ВКонтакте на Вашем сайте. Кроме этого, с согласия пользователей, вы сможете получить доступ к информации об их друзьях, фотографиях, аудиозаписях, видеороликах и прочих данных ВКонтакте для более глубокой интеграции с Вашим проектом.
Т.е. это JS библиотека, позволяющая работать с VK API (авторизация, вызов методов API, вроде 'wall.post', 'audio.get', 'video.add', etc…) прямо со страницы вашего сайта. Для того, чтобы использовать эту библиотеку, необходимо создать VK-приложение с типом «Веб-сайт», указать домен в настройках, и разместить пару тегов script на странице.
Подключение библиотеки
Пример подключения и инициализации библиотеки:
Естественно, в параметре appId
можно указать только ID VK-приложения, в настройках которого «Базовый домен» совпадает с доменом страницы, на котором мы подключаем библиотеку.
Наша страница может обращаться к методам VK API после того, как пользователь во всплывающем окне разрешит VK-приложению доступ к своему профилю. Для того, чтобы показать это всплывающее окно, нужно вызвать метод VK.Auth.login()
. И после того, как разрешение получено, можно обращаться к VK API. Важное замечание: если пользователь однажды предоставил приложению доступ к своему профилю, то даже после перезагрузки страницы его разрешение остается в силе: не нужно каждый раз вызывать VK.Auth.login()
. Для того, чтобы определить, нужно ли просить пользователя предоставить сайту (точнее, VK-приложению сайта) доступ к своему профилю, можно использовать следующий код:
VK.Auth.getLoginStatus(function(resp) {
if (resp.session) {
// Пользователь уже предоставил доступ к своему профилю.
// Можно спокойно работать с VK API.
} else {
// Нужно просить пользователя предоставить доступ,
// и только после его согласия работать с VK API.
VK.Auth.login(...);
}
});
Если при вызове VK.init()
указать ID чужого приложения, домен которого не совпадает с доменом страницы, на котором запускается библиотека — ничего работать не должно (даже функция-callback, переданная в getLoginStatus()
не будет вызвана).
Небольшая оговорка: оказывается, этот запрет можно обойти. Для того, чтобы было понятнее, вкратце расскажу, как работает проверка «авторизованности» пользователя в VK-приложении.
Принцип проверки авторизации пользователя
Для работы с VK API из JS-кода веб-страницы, используется метод VK.Api.call()
, например:
// Получение информации о текущем пользователе
VK.Api.call('users.get', {}, function(result) {
var user;
if (result.response) {
user = result.response[0];
alert('Здравствуйте, ' + user.first_name + ' ' + user.last_name + '!');
}
});
При первом вызове метода VK.Api.call()
, библиотека обращается на бекенд VK за Access Token’ом. Для этого, внутри VK.Api.call()
вызывается метод VK.Auth.getLoginStatus()
, через который библиотека и получает этот токен (конечно, если только пользователь ранее предоставил доступ сайту к своему профилю). После того, как токен удалось получить, происходит запрос к API и получение ответа от сервера. Уязвимость кроется в способе получения и способе обработки ответа сервера в методе VK.Auth.getLoginStatus()
. Всему виной JSONP, вернее, его некорректное применение.
Порочный JSONP
Давайте подробнее рассмотрим работу метода VK.Auth.getLoginStatus()
. Для того, чтобы получить Access Token, делается JSONP-запрос на следующий URL:
https://login.vk.com/?act=openapi&oauth=1&aid=1234567&location=www.example.com&rnd=456
Параметры:
aid
— ID приложенияlocation
— домен, с которого выполняется запросrnd
— ID callback-функции (ведь это JSONP)
Если в запросе по URL, приведённом выше, домен в HTTP Referrer совпадает с доменом, который был указан в настройках VK-приложения, или если HTTP Referrer не передавать совсем (!) — то получаем такой ответ:
/* */
if (location.hostname != 'www.example.com') {
window.location.href = 'http://vk.com/oauth';
for (;;);
} else {
VK.Auth.lsCb[456]({
"auth": true,
"access_token": "5ea11111d799a53236f5d3eff5d34bcd2dda0f9e6a7aaf743f7d26d3487456f6ce8d5e1ff82eaa6f7b04a",
"expire": 1436755095,
"time": 7200,
"sig": "12d254526496a6db2af6bed2eb1dd3e7",
"secret": "oauth",
"user": {
"id": "%ID_страницы%",
"domain": "%имя_страницы%",
"href": "https:\/\/vk.com\/%имя_или_id_страницы%",
"first_name": "%имя%",
"last_name": "%фамилия%",
"nickname": ""
}
});
}
Важно: При JSONP-запросе на вышеуказанный URL, браузер также отправляет куки пользователя. Поэтому, сервер знает, от имени какого пользователя VK делается запрос, и строит ответ исходя из этой информации.
Как я уже говорил раннее, ответом является JS-код, в котором следующая логика: если домен текущей страницы (location.hostname
) равен домену, указанному в настройках приложения — вызываем функцию VK.Auth.lsCb[%значение_параметра_rnd%]()
, и в качестве первого аргумента передаём объект с Access Token’ом, иначе — перенаправляем пользователя на http://vk.com/oauth
. Зачем? Это такая защита. Т.к. если бы домен, указанный в настройках VK-приложения не сверялся с location.hostname
, то любой мог бы разместить у себя на сайте следующий код: