Подводные камни при передаче JSON в JavaScript
Часто при генерации страницы нужно передать какие-либо данные окружению JavaScript в браузере пользователя.
Скажем, на странице нужна информация об авторизованном пользователе:
{
"id": 42,
"username": "vovan2007",
"name": "Вовочка"
}
Асинхронная загрузка
Можно пойти по пути наименьшего сопротивления и достать эти данные аяксом:
$.getJSON('/ajax/loggeduser').then(function(info){
// Делаем что-то полезное
});
Или так, если вы впереди планеты всей:
window.fetch('/ajax/loggeduser').then(function(info){
// Делаем что-то полезное
});
Всё. Нужны данные — грузим их. Не нужны — экономим пользователю трафик. Все счастливы.
Или не все? Внезапно может оказаться, что данные профиля на странице нужны всегда, и на каждый обычный запрос страницы требуется ещё один аякс-запрос.
Сервер не рад, потому что мы только что удвоили количество запросов.
Клиент не рад, потому что пока запрос не выполнится, он не сможет полноценно воспользоваться страницей. Да и накладных расходов (HTTP-заголовки тоже занимают место!) больше, чем самих данных.
Похоже, пришло время внедрить профиль пользователя прямо в код.
Внедрённые данные
В этом случае мы просто будем вставлять на страницу что-то вроде такой конструкции:
<script>
var userInfo = {{ user.info | json }};
</script>
Это синтаксис JINJA, но поверьте, неважно, какой шаблонизатор вы используете.
Что получится на выходе:
<script>
var userInfo = {
"id": 42,
"username": "vovan2007",
"name": "Вовочка"
};
</script>
Всё круто, мы сэкономили один запрос, и в глобальной переменной userInfo
лежит профиль Вовочки. Но это ещё не всё.
Подводные камни
Камень первый: HTML-парсер
Что будет, если вместо Вовочки нам попадётся злоумышленник, который воспользуется
тем, что в поле name
можно писать что угодно?
Тогда при попытке заинлайнить
{
"id": 99,
"username": "Haxx0r",
"name": "Вася</script><script>alert('Pwnd!')//"
}
мы получим:
<script>
var userInfo = {
"id": 99,
"username": "Haxx0r",
"name": "Вася
</script>
<script>
alert('Pwnd!')//"};
</script>
В самом деле, HTML-парсер может узнать о том, что блок JS-кода закончился только по
строке </script>
. И его не сильно волнует, что она находится прямо
посреди внедрённого JSON.
Возможным решением может быть замена </script
на <\/script
при выводе.
JavaScript проигнорирует экранирование слеша, а вот парсер HTML уже не будет
давать ложных срабатываний. И хакер Вася останется с носом:
<script>
var userInfo = {
"id": 99,
"username": "Haxx0r",
"name": "Вася<\/script><script>alert('Pwnd!')//"
};
</script>
Камень второй: Юникод
Само название JSON гласит, «Запись объекта как в JavaScript» (JavaScript Object Notation). И казалось бы, из любого JSON мы всегда получим валидный JavaScript.
Но это не так! Дело в том, что символы U+2028
(разделитель строк) и U+2029
(разделитель абзацев) в JS считаются переносами строк, в JSON — нет.
Иными словами, U+2028
и U+2029
не создадут проблем в JSON:
{
"id": 115,
"username": "unicode",
"name": "Юни•код"
};
А вот в JS они вызовут ошибку «незавершённая строка»:
<script>
var userInfo = {
"id": 115,
"username": "unicode",
"name": "Юни
код"
}
</script>
К счастью, таким образом можно только обрушить страницу, а вот запустить зловредный код — нельзя. Или можно? Напишите, пожалуйста, в комментариях!
Как решить эту проблему? А точно так же: перед выводом на страницу искать в строке символы
с кодами 0x2028
и 0x2029
и заменять их на \u2028
и \u2029
соответственно.
Заключение
Разумеется, превращение JSON в JavaScript — это не единственный способ передать данные на страницу.
Можно, например, поступить как Twitter, и вставлять JSON в атрибут какого-нибудь элемента:
<input type="hidden" id="init-data" class="json-data"
value="{"permalinkOverlayEnabled":false, . . .">
Однако, вставка JSON прямо в тег <script>
не такой уж плохой способ. Если,
конечно, вы помните о двух важных нюансах:
</script>
- U+2028 и U+2029