Подводные камни при передаче 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="{&quot;permalinkOverlayEnabled&quot;:false, . . .">

Однако, вставка JSON прямо в тег <script> не такой уж плохой способ. Если, конечно, вы помните о двух важных нюансах: