XSS с использованием JSONP и jQuery

Владимир | | Ajax, JavaScript, PHP, Web разработка.

jsonp jquery

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

Как несложно догадаться, JSONP представляет собой расширение формата JSON. Расшифровывается эта аббревиатура так — JavaScript Object Notation with Padding, т.е. JSON с дополнением.

Об этом дополнении чуть ниже, а сейчас, напомню, что представляет собой формат JSON и зачем он нужен.

Если не вникать в нюансы, JSON представляет собой формат записи JavaScript объектов в виде обычной текстовой строки. Т.е. вы можете отправить такую строку в ответ на AJAX запрос, а затем преобразовать её на стороне клиента в объект с данными.

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

Примечание. Я уже публиковал несколько постов о JSON с примерами, например, этот и этот.

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

Для того, чтобы понять, что это даёт, взгляните на следующий JavaScript код:

function test(data) {
	alert(data.var1);
}
test({"var1":"123"});

Последняя строка записана в формате JSONP. И в тоже время она представляет собой JavaScript функции! Если отбросить текст в начале строки (test) и круглые скобки, то получим строку в формате JSON.

В результате работы этого скрипта будет вызвана функция test и в параметре data она получит объект с вашими данными. Т.е. вы увидите текст «123».

Сейчас, наверное, польза от JSONP не очень очевидна, но всё меняется, когда возникает необходимость отправлять XSS запросы.

Проблема в том, что Same Origin Policy запрещает отправлять запросы ко всем ресурсам кроме того, с которого был загружен сам скрипт. Для обхода этого ограничения используется вставка дополнительного тега script в заголовок страницы. Атрибут src этого тега должен указывать на домен, к которому вы хотите отправить запрос.

Примечание. Подробнее о вариантах отправки XSS запросов можно почитать в статье «XSS и Same Origin Policy».

Иными словами, вы явно загружаете скрипт с нужного домена. А из этого скрипта можно отправлять запросы к этому домену, не нарушая Same Origin Policy.

Теперь разберем, что нам даёт JSONP, и тут же рассмотрим его поддержку в jQuery. Если кратко – с его помощью мы можем контролировать процесс загрузки данных и вызвать собственную JavaScript функцию после его завершения.

Самое главное, нужно четко понимать, что формат JSONP должен поддерживаться на стороне сервера. Ведь разработчик серверного API не может заранее определить, как будет называться JavaScript функция, обрабатывающая полученные данные. Т.е. заранее неизвестно какой текст нужно вставлять перед открывающей круглой скобкой.

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

1) Клиентский скрипт создает тег script и в его атрибуте src указывает адрес скрипта вместе с названием функции-обработчика в одном из параметров. Например,

<script src=”http://anothersite.domen/jsondata.php?callback=test”></script>

Здесь, test – имя функции-обработчика, а jsondata.php – серверный скрипт, который создает строку в формате JSONP.

2) Браузер загружает скрипт, т.е. выполняет запрос, указанный в атрибуте src.

3) Серверный скрипт читает параметр callback, добавляет его значение в начало JSON строки и возвращает результат.

4) После загрузки данных, клиентский скрипт выполняет функцию-обработчик и в качестве параметра передает ей данные, полученные от сервера.

5) Последний этап – удаление уже не нужного тега script.

Довольно сложный алгоритм получается. Но, фокус в том, что jQuery сделает 90% работы за вас 😉

Оценить всю пользу от jQuery проще всего на примере.

Клиентский скрипт.

$.getJSON("http://www.wp25.l/jsondata.php?callback=?",
	function(data) {
		alert(data.myval);
	});

Серверный скрипт.

<?php
$callback = $_GET['callback'];
echo $callback.'('."{'myval':'1234567890'}".')';
?>

Т.е. чтобы использовать JSONP вам нужно вызвать функцю getJSON. В её первом параметре указать запрос, но вместо имени функции-обработчика ставим знак вопроса. Во втором параметре передаём саму функцию-обработчик. Все остальное jQuery делает за вас.

Серверный скрипт формирует JSON строку с данными (в реальном приложении, эти данные, скорее всего, будут получены из базы) и добавляет к ней значение, переданное в параметре callback и круглые скобки.

Чтобы наглядно проиллюстрировать работу jQuery я остановил выполнение скрипта на вызове функции alert и сделал скриншот.

Как видите, в заголовке страницы появился дополнительный тег script, а знак вопроса (в параметре callback) изменился на jsonp1234988224763. После завершения работы функции тег script исчезает.

Тут у вас может возникнуть вопрос. Зачем придумывать какой-то формат данных, если можно получить тот же результат, используя функцию getScript?

Действительно, принцип работы этих функций (getScript и getJSON) очень похож. Точнее не отличается реализация XSS запросов.

Но есть принципиальная разница. Функция getScript получает JavaScript код и выполняет его. getJSON – пытается преобразовать полученные данные в JavaScript объект. Если нарушить формат – возникнет ошибка.

Теперь представьте ситуацию. Вы используете чей-нибудь сервис и загружаете данные с помощью getScript. Администрация этого сервиса может вставить на вашу страницу любой JS код. Например, им ничто не помешает получить файлы cookies ваших посетителей.

Работа с getJSON выглядит безопаснее. Во всяком случае, данные будут соответствовать формату, и вы сами решаете, как их использовать. Можно, например, вставить дополнительные проверки.

Хотя, возможно, и JSON формат имеет уязвимости. Правда, я о них не слышал 😉 Если есть идеи, пишите, мне будет интересно почитать 😉

  • Big_Shark

    Я использую так
    $.getJSON("http://www.wp25.l/jsondata.php",
    function(data) {
    alert(data.myval);
    });


    и все работает интересно какие от такого использования могут быть плюсы и минусы.

  • Big_Shark

    Я использую так
    $.getJSON("http://www.wp25.l/jsondata.php",
    function(data) {
    alert(data.myval);
    });


    и все работает интересно какие от такого использования могут быть плюсы и минусы.

  • Big_Shark


    Вырезало

  • Big_Shark


    Вырезало

  • Big_Shark

    php код вырезает
    использую так
    echo '('.»{'myval':'1234567890'}».')';

    • У меня такой вариант не работает. Если убираю ?callback=?, то jQuery пытается отправить запрос напрямую и возникает «Access to restricted URI denied».
      Если убираю только значение параметра callback, т.е. в PHP скрипте остается только строка
      echo $callback.'('.»{'myval':'1234567890'}».')';
      то не вызывается функция-обработчик.
      Во всех экспериментах использовал FF3.

      • Big_Shark

        Я обращаюсь к своему скрипту находящемся на том же домени.

        • Если домен тот же, то проблемы вообще нет. Запросы отправляются напрямую.

  • Big_Shark

    php код вырезает
    использую так
    echo '('.»{'myval':'1234567890'}».')';

    • У меня такой вариант не работает. Если убираю ?callback=?, то jQuery пытается отправить запрос напрямую и возникает «Access to restricted URI denied».
      Если убираю только значение параметра callback, т.е. в PHP скрипте остается только строка
      echo $callback.'('.»{'myval':'1234567890'}».')';
      то не вызывается функция-обработчик.
      Во всех экспериментах использовал FF3.

      • Big_Shark

        Я обращаюсь к своему скрипту находящемся на том же домени.

        • Если домен тот же, то проблемы вообще нет. Запросы отправляются напрямую.

  • Хорошая идея насчет выдачи результатов в JSON, на днях прикрутим к API

    • Лучше в JSONP 🙂

      • Я его и имел ввиду, написал короче 🙂

        • Ясно 🙂 , будет нужна помощь с тестированием, обращайтесь.

        • Прикрутили, вроде работает
          GeoIP в форматах JSON и JSONP

        • Отличное решение 😉
          Кстати, в параметре zoom максимально возможное приближение карт Google Maps для данного региона?

        • нет, там оптимальное приближение, когда страна входит полностью в область видимости карты размера как здесь: карта Швейцарии

        • Ясно. Тогда, по-моему, я нашел ошибку 🙂
          http://www.wipmania.com/ru/map/RU/
          у меня открылась вся карта мира, т.е. видны все континенты. Россия, конечно, страна большая, но не настолько 😉

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

          Похоже, такими темпами у вас скоро будет лучшая геолокационная база данных. Удачи!

        • Хм…я открыл карту и мне показалась Россия во все

        • Хм…я открыл карту и мне показалась Россия во всей красе, ни больше ни меньше. При приближении обрезается, при отдалении — показывается больше чем надо. А кроме евразии захватывается только кусочек африки.
          Про геолокацию — чуть-чуть приоткрою тайну 😉 Скоро сменится весь комплекс-движок обновления и проверки базы. Все будет много больше, лучше и точнее.

        • Про разницу между щвейцарией и США — это к гуглу 🙂

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

          это к гуглу

          Вряд ли они что-то смогут сделать. Площадь стран так просто не изменишь 🙂

    • Big_Shark

      Ну вот и нашлось как расширить едро API

  • Хорошая идея насчет выдачи результатов в JSON, на днях прикрутим к API

    • Лучше в JSONP 🙂

      • Я его и имел ввиду, написал короче 🙂

        • Ясно 🙂 , будет нужна помощь с тестированием, обращайтесь.

        • Прикрутили, вроде работает
          GeoIP в форматах JSON и JSONP

        • Отличное решение 😉
          Кстати, в параметре zoom максимально возможное приближение карт Google Maps для данного региона?

        • нет, там оптимальное приближение, когда страна входит полностью в область видимости карты размера как здесь: карта Швейцарии

        • Ясно. Тогда, по-моему, я нашел ошибку 🙂
          http://www.wipmania.com/ru/map/RU/
          у меня открылась вся карта мира, т.е. видны все континенты. Россия, конечно, страна большая, но не настолько 😉

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

          Похоже, такими темпами у вас скоро будет лучшая геолокационная база данных. Удачи!

        • Хм…я открыл карту и мне показалась Россия во все

        • Хм…я открыл карту и мне показалась Россия во всей красе, ни больше ни меньше. При приближении обрезается, при отдалении — показывается больше чем надо. А кроме евразии захватывается только кусочек африки.
          Про геолокацию — чуть-чуть приоткрою тайну 😉 Скоро сменится весь комплекс-движок обновления и проверки базы. Все будет много больше, лучше и точнее.

        • Про разницу между щвейцарией и США — это к гуглу 🙂

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

          это к гуглу

          Вряд ли они что-то смогут сделать. Площадь стран так просто не изменишь 🙂

    • Big_Shark

      Ну вот и нашлось как расширить едро API

  • MX

    Работа с getJSON выглядит безопаснее. Во всяком случае, данные будут соответствовать формату, и вы сами решаете, как их использовать. Можно, например, вставить дополнительные проверки.

    В каком месте getJSON безопаснее getScript? Что мешает JSONP-сервису кроме вызова callback-фукции клиента сделать свое «черное дело» например, спереть те же самые куки или заставить клиента выполнить некий злонамеренный код?

    • Big_Shark

      getScript сразу выполняет скрипт а getJSON смотрит объект вида json и если его там нету или он с ошибкой то она ничего не выполняет то есть и другой скрипт не будет вызываться.

    • JSONP-сервис ваши функции не вызывает и вызывать не может. Он передает вам строку текста. Тоже самое вы получаете и при вызове getScript. Разница в том, что при получении данных через JSONP можно выполнить проверку на соответствие формату JSON, а как проверять код скрипта непонятно.
      Единственная уязвимость, которую я могу придумать, заключается в передаче заведомо некорректных данных. Т.е. если вы недостаточно внимательно проверяете полученные данные, то теоретически некорректные значения могут привести к сбоям в работе приложения и возникновению уязвимости. Но, во-первых, тут многое зависит от конкретного приложения, а, во-вторых, это уязвимость вашего кода, а не формата.

  • MX

    Работа с getJSON выглядит безопаснее. Во всяком случае, данные будут соответствовать формату, и вы сами решаете, как их использовать. Можно, например, вставить дополнительные проверки.

    В каком месте getJSON безопаснее getScript? Что мешает JSONP-сервису кроме вызова callback-фукции клиента сделать свое «черное дело» например, спереть те же самые куки или заставить клиента выполнить некий злонамеренный код?

    • Big_Shark

      getScript сразу выполняет скрипт а getJSON смотрит объект вида json и если его там нету или он с ошибкой то она ничего не выполняет то есть и другой скрипт не будет вызываться.

    • JSONP-сервис ваши функции не вызывает и вызывать не может. Он передает вам строку текста. Тоже самое вы получаете и при вызове getScript. Разница в том, что при получении данных через JSONP можно выполнить проверку на соответствие формату JSON, а как проверять код скрипта непонятно.
      Единственная уязвимость, которую я могу придумать, заключается в передаче заведомо некорректных данных. Т.е. если вы недостаточно внимательно проверяете полученные данные, то теоретически некорректные значения могут привести к сбоям в работе приложения и возникновению уязвимости. Но, во-первых, тут многое зависит от конкретного приложения, а, во-вторых, это уязвимость вашего кода, а не формата.

  • Спасибо огромное автору за статью! Я пол-нета перерыл 🙂 Это как раз то, что я искал!

  • Спасибо огромное автору за статью! Я пол-нета перерыл 🙂 Это как раз то, что я искал!

  • Onagr

    Вы плохо понимаете как работает JSONP — это просто загрузка стороннего скрипта в котором может быть что угодно, конечно включен и вызов вашей callback-функции с объектом-параметром. Безопасности нет ни какой, вы должны знать что и откуда загружаете.

  • Большое спасибо за комментарий.
    Сейчас провел пару экспериментов, действительно, передать можно любой код. Почему-то я думал, что getJSON выполняет проверку формата JSON.

  • Xx

    Я так понял что jsonp не отправить данные методом post ?

    • Нет, вы можете отправлять данные любым способом. Главное, чтобы серверный скрипт об этом знал. JSONP оговаривает только то, что ответ в формате JSON будет «завернут» в вызов функции. Каким образом вы передадите имя этой функции и любые другие дополнительные параметры — дело ваше. Конечно, если вы хотите получать данные от стороннего сервиса, который работает только с GET параметрами, то POST не пройдет.

      Кстати, метод $.getJSON это обертка для $.ajax, который может передавать данные любым способом.

      • Xx

        $.ajax({
        url:»http://www.vashesoznanie.ru?x=1",
        data:$(«#form»).serialize(),
        dataType:»jsonp»,
        type:»POST»
        });

        На сервере:
        var_dump($_GET); // Покажет что $_GET=array('x'=>'1','y'=>'2');
        var_dump($_POST); // Покажет что $_POST=array();

        Почему?

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

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

          Как только вы указываете dataType:»jsonp» вместо отправки ajax запроса jquery создает на странице тег script и браузер сам отправляет запрос. В атрибуте src тега script указать POST параметры нельзя, поэтому все значения приходят в массиве GET.

  • salavey

    Статье уже 4 года, а актуальная)) Спасибо, помогло))