Перейти к основному содержимому

Подмножество JSON, также известное как JSON ⊂ ECMAScript

· 6 мин. чтения
Матиас Биненс ([@mathias](https://twitter.com/mathias))

Благодаря предложению JSON ⊂ ECMAScript, JSON стал синтаксическим подмножеством ECMAScript. Если вас удивляет, что это не было так ранее, вы не одиноки!

Старое поведение из ES2018

В ES2018 строковые литералы ECMAScript не могли содержать неизбежные символы U+2028 LINE SEPARATOR и U+2029 PARAGRAPH SEPARATOR, поскольку они считаются разделителями строк даже в таком контексте:

// Строка, содержащая необработанный символ U+2028.
const LS = '
';
// → ES2018: SyntaxError

// Строка, содержащая необработанный символ U+2029, созданная с помощью `eval`:
const PS = eval('"\u2029"');
// → ES2018: SyntaxError

Это проблематично, потому что строки JSON могут содержать эти символы. В результате разработчики были вынуждены реализовать специальную логику постобработки при встраивании допустимого JSON в программы ECMAScript для обработки этих символов. Без такой логики в коде могли быть тонкие ошибки или даже проблемы безопасности!

Новое поведение

В ES2019 строковые литералы теперь могут содержать необработанные символы U+2028 и U+2029, устраняя путаницу между ECMAScript и JSON.

// Строка, содержащая необработанный символ U+2028.
const LS = '
';
// → ES2018: SyntaxError
// → ES2019: нет исключения

// Строка, содержащая необработанный символ U+2029, созданная с помощью `eval`:
const PS = eval('"\u2029"');
// → ES2018: SyntaxError
// → ES2019: нет исключения

Это небольшое улучшение значительно упрощает ментальную модель для разработчиков (на один краевой случай меньше для запоминания!) и уменьшает необходимость в специальной логике постобработки при встраивании допустимого JSON в программы ECMAScript.

Встраивание JSON в программы на JavaScript

Благодаря этому предложению, JSON.stringify теперь можно использовать для генерации допустимых строковых литералов ECMAScript, литералов объектов и литералов массивов. А благодаря отдельному предложению корректного JSON.stringify, эти литералы можно безопасно представлять в формате UTF-8 и других кодировках (что полезно, если вы собираетесь записать их в файл на диск). Это очень полезно для случаев метапрограммирования, таких как динамическое создание исходного кода на JavaScript и его запись на диск.

Вот пример создания допустимой программы на JavaScript, встраивающей заданный объект данных, используя преимущество того, что грамматика JSON теперь является подмножеством ECMAScript:

// Объект JavaScript (или массив, или строка), представляющий некоторые данные.
const data = {
LineTerminators: '\n\r

',
// Примечание: строка содержит 4 символа: '\n\r\u2028\u2029'.
};

// Преобразуйте данные в их JSON-строковый вид. Благодаря JSON ⊂
// ECMAScript выход `JSON.stringify` гарантированно будет
// синтаксически допустимым литералом ECMAScript:
const jsObjectLiteral = JSON.stringify(data);

// Создайте допустимую программу ECMAScript, которая встраивает данные
// в виде объекта-литерала.
const program = `const data = ${ jsObjectLiteral };`;
// → 'const data = {"LineTerminators":"…"};'
// (Дополнительное экранирование необходимо, если целевой объект — это встроенный <script>.)

// Запишите файл, содержащий программу ECMAScript, на диск.
saveToDisk(filePath, program);

Вышеприведенный сценарий создает следующий код, который оценивается в эквивалентный объект:

const data = {"LineTerminators":"\n\r

"};

Встраивание JSON в программы на JavaScript с помощью JSON.parse

Как объяснено в цена JSON, вместо внедрения данных как объектных литералов JavaScript, как показано ниже:

const data = { foo: 42, bar: 1337 }; // 🐌

…данные можно представить в виде JSON-строкового изображения, а затем разобрать JSON на этапе выполнения для повышения производительности в случае больших объектов (10 кБ+):

const data = JSON.parse('{"foo":42,"bar":1337}'); // 🚀

Вот пример реализации:

// Объект JavaScript (или массив, или строка), представляющий некоторые данные.
const data = {
LineTerminators: '\n\r

',
// Примечание: строка содержит 4 символа: '\n\r\u2028\u2029'.
};

// Преобразуйте данные в их JSON-строковый вид.
const json = JSON.stringify(data);

// Теперь мы хотим вставить JSON в тело скрипта как строковый литерал JavaScript
// согласно https://v8.dev/blog/cost-of-javascript-2019#json,
// экранируя специальные символы, такие как `"` в данных.
// Благодаря JSON ⊂ ECMAScript выход `JSON.stringify`
// гарантированно будет синтаксически корректным литералом ECMAScript:
const jsStringLiteral = JSON.stringify(json);
// Создайте допустимую программу ECMAScript, которая встраивает строковый литерал JavaScript,
// представляющий данные JSON, в вызов `JSON.parse`.
const program = `const data = JSON.parse(${ jsStringLiteral });`;
// → 'const data = JSON.parse("…");'
// (Дополнительное экранирование необходимо, если цель — встроенный <script>.)

// Запишите файл, содержащий программу ECMAScript, на диск.
saveToDisk(filePath, program);

Приведенный выше скрипт генерирует следующий код, который интерпретируется как эквивалентный объект:

const data = JSON.parse("{\"LineTerminators\":\"\\n\\r

\"}");

Бенчмарк Google, сравнивающий JSON.parse с литералами объектов JavaScript использует эту технику на этапе сборки. Функция Chrome DevTools “скопировать как JS” была значительно упрощена благодаря аналогичной технике.

Замечание о безопасности

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

Однако U+2028 и U+2029 по-прежнему считаются символами терминаторов строк в других частях грамматики ECMAScript. Это означает, что есть случаи, где внедрение JSON в программы JavaScript остается небезопасным. Рассмотрим следующий пример, где сервер вставляет контент, предоставленный пользователем, в HTML-ответ после обработки его через JSON.stringify():

<script>
// Информация для отладки:
// User-Agent: <%= JSON.stringify(ua) %>
</script>

Обратите внимание, что результат выполнения JSON.stringify вставляется в однострочный комментарий внутри скрипта.

При использовании, как в приведенном выше примере, JSON.stringify() гарантированно возвращает одну строку. Проблема в том, что понятие «одной строки» отличается между JSON и ECMAScript. Если ua содержит неэкранированный символ U+2028 или U+2029, это нарушает однострочный комментарий и выполняет остальную часть ua как исходный код JavaScript:

<script>
// Информация для отладки:
// User-Agent: "Строка от пользователя<U+2028> alert('XSS');//"
</script>
<!-- …эквивалентно: -->
<script>
// Информация для отладки:
// User-Agent: "Строка от пользователя
alert('XSS');//"
</script>
примечание

Примечание: В приведенном выше примере неэкранированный символ U+2028 представлен как <U+2028> для облегчения восприятия.

JSON ⊂ ECMAScript не помогает в данном случае, так как это влияет только на строковые литералы — а в данной ситуации вывод JSON.stringify вводится в позицию, где он не создает напрямую строковый литерал JavaScript.

Если для этих двух символов не вводится специальная постобработка, приведенный выше код представляет собой уязвимость межсайтового скриптинга (XSS)!

примечание

Примечание: Крайне важно обрабатывать пользовательский ввод, чтобы экранировать любые специальные последовательности символов в зависимости от контекста. В данном конкретном случае мы внедряем данные в <script>-тег, поэтому мы также должны экранировать </script, <script, и <!-​-.

Поддержка JSON ⊂ ECMAScript