Aller au contenu principal

Incorporer JSON, autrement dit JSON ⊂ ECMAScript

· 7 minutes de lecture
Mathias Bynens ([@mathias](https://twitter.com/mathias))

Avec la proposition JSON ⊂ ECMAScript, JSON devient un sous-ensemble syntaxique d'ECMAScript. Si vous êtes surpris que cela n'était pas déjà le cas, vous n'êtes pas le seul !

Le comportement ancien d'ES2018

En ES2018, les littéraux de chaîne de caractères d'ECMAScript ne pouvaient pas contenir les caractères séparateurs de ligne U+2028 LINE SEPARATOR et U+2029 PARAGRAPH SEPARATOR non échappés, car ils sont considérés comme des terminaux de ligne même dans ce contexte :

// Une chaîne contenant un caractère U+2028 brut.
const LS = '
';
// → ES2018 : SyntaxError

// Une chaîne contenant un caractère U+2029 brut, produit par `eval` :
const PS = eval('"\u2029"');
// → ES2018 : SyntaxError

Cela posait problème car les chaînes JSON peuvent contenir ces caractères. En conséquence, les développeurs devaient implémenter une logique de post-traitement spécialisée lors de l'intégration de JSON valide dans des programmes ECMAScript pour gérer ces caractères. Sans cette logique, le code pouvait comporter des bugs subtils, voire des problèmes de sécurité !

Le nouveau comportement

En ES2019, les littéraux de chaîne peuvent désormais contenir les caractères U+2028 et U+2029 bruts, éliminant ainsi la confusion d'incohérence entre ECMAScript et JSON.

// Une chaîne contenant un caractère U+2028 brut.
const LS = '
';
// → ES2018 : SyntaxError
// → ES2019 : pas d'exception

// Une chaîne contenant un caractère U+2029 brut, produit par `eval` :
const PS = eval('"\u2029"');
// → ES2018 : SyntaxError
// → ES2019 : pas d'exception

Cette petite amélioration simplifie grandement le modèle mental pour les développeurs (une complication de moins à retenir !), et réduit le besoin de logique de post-traitement spécialisée lors de l'intégration de JSON valide dans des programmes ECMAScript.

Intégrer JSON dans des programmes JavaScript

Grâce à cette proposition, JSON.stringify peut désormais être utilisé pour générer des littéraux de chaînes ECMAScript valides, des littéraux d'objet et des littéraux de tableau. Et grâce à la proposition distincte JSON.stringify bien formé, ces littéraux peuvent être représentés en toute sécurité en UTF-8 et d'autres encodages (pratique si vous souhaitez les écrire dans un fichier sur disque). Cela est extrêmement utile pour les cas d'utilisation liés à la métaprogrammation, comme la création dynamique de code source JavaScript et son écriture sur disque.

Voici un exemple de création d'un programme JavaScript valide intégrant un objet de données donné, en tirant parti de la grammaire JSON qui est désormais un sous-ensemble d'ECMAScript :

// Un objet JavaScript (ou tableau, ou chaîne) représentant des données.
const data = {
LineTerminators: '\n\r

',
// Remarque : la chaîne contient 4 caractères : '\n\r\u2028\u2029'.
};

// Transformez les données en leur forme JSON-stringifiée. Grâce à JSON ⊂
// ECMAScript, la sortie de `JSON.stringify` est garantie d'être
// un littéral ECMAScript syntaxiquement valide :
const jsObjectLiteral = JSON.stringify(data);

// Créez un programme ECMAScript valide qui intègre les données comme un objet
// littéral.
const program = `const data = ${ jsObjectLiteral };`;
// → 'const data = {"LineTerminators":"…"};'
// (Un échappement supplémentaire est nécessaire si la cible est un <script> inline.)

// Écrivez un fichier contenant le programme ECMAScript sur disque.
saveToDisk(filePath, program);

Le script ci-dessus produit le code suivant, qui s'évalue à un objet équivalent :

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

"};

Intégrer JSON dans des programmes JavaScript avec JSON.parse

Comme expliqué dans le coût du JSON, au lieu d'intégrer les données comme un littéral d'objet JavaScript, comme ceci :

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

…les données peuvent être représentées sous forme JSON-stringifiée, puis analysées avec JSON.parse au moment de l'exécution, pour de meilleures performances dans le cas d'objets volumineux (10 kB+):

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

Voici un exemple d'implémentation :

// Un objet JavaScript (ou tableau, ou chaîne) représentant des données.
const data = {
LineTerminators: '\n\r

',
// Remarque : la chaîne contient 4 caractères : '\n\r\u2028\u2029'.
};

// Transformez les données en leur forme JSON-stringifiée.
const json = JSON.stringify(data);

// Maintenant, nous voulons insérer le JSON dans un corps de script en tant que
// littéral de chaîne JavaScript selon https://v8.dev/blog/cost-of-javascript-2019#json,
// en échappant les caractères spéciaux comme `\"` dans les données.
// Grâce à JSON ⊂ ECMAScript, la sortie de `JSON.stringify` est
// garantie d'être un littéral ECMAScript syntaxiquement valide :
const jsStringLiteral = JSON.stringify(json);
// Créez un programme ECMAScript valide qui intègre le littéral de chaîne
// JavaScript représentant les données JSON dans un appel `JSON.parse`.
const program = `const data = JSON.parse(${ jsStringLiteral });`;
// → 'const data = JSON.parse("…");'
// (Un échappement supplémentaire est nécessaire si la cible est un <script> en ligne.)

// Écrire un fichier contenant le programme ECMAScript sur le disque.
saveToDisk(filePath, program);

Le script ci-dessus produit le code suivant, qui évalue à un objet équivalent :

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

\"}");

Le benchmark de Google comparant JSON.parse avec les littéraux d'objet JavaScript utilise cette technique dans son étape de construction. La fonctionnalité de Chrome DevTools "copier en JS" a été considérablement simplifiée en adoptant une technique similaire.

Une note sur la sécurité

JSON ⊂ ECMAScript réduit le décalage entre JSON et ECMAScript dans le cas des littéraux de chaîne spécifiquement. Étant donné que les littéraux de chaîne peuvent apparaître dans d'autres structures de données prises en charge par JSON, telles que les objets et les tableaux, cela résout également ces cas, comme le montrent les exemples de code ci-dessus.

Cependant, U+2028 et U+2029 sont toujours traités comme des caractères de terminaison de ligne dans d'autres parties de la grammaire ECMAScript. Cela signifie qu'il existe encore des cas où il est dangereux d'injecter du JSON dans des programmes JavaScript. Considérons cet exemple, où un serveur injecte un contenu fourni par l'utilisateur dans une réponse HTML après l'avoir exécuté via JSON.stringify() :

<script>
// Infos de débogage :
// User-Agent: <%= JSON.stringify(ua) %>
</script>

Notez que le résultat de JSON.stringify est injecté dans un commentaire sur une seule ligne dans le script.

Lorsqu'il est utilisé comme dans l'exemple ci-dessus, JSON.stringify() est garanti de renvoyer une seule ligne. Le problème est que ce qui constitue une "seule ligne" diffère entre JSON et ECMAScript. Si ua contient un caractère U+2028 ou U+2029 non échappé, nous sortons du commentaire sur une seule ligne et exécutons le reste de ua comme code source JavaScript :

<script>
// Infos de débogage :
// User-Agent: "Chaîne fournie par l'utilisateur<U+2028> alert('XSS');//"
</script>
<!-- …est équivalent à : -->
<script>
// Infos de débogage :
// User-Agent: "Chaîne fournie par l'utilisateur
alert('XSS');//"
</script>
remarque

Note : Dans l'exemple ci-dessus, le caractère brut U+2028 non échappé est représenté sous la forme <U+2028> pour le rendre plus facile à suivre.

JSON ⊂ ECMAScript n'aide pas ici, car il n'impacte que les littéraux de chaîne — et dans ce cas, la sortie de JSON.stringify est injectée à une position où elle ne produit pas directement un littéral de chaîne JavaScript.

Sauf si un post-traitement spécial pour ces deux caractères est introduit, le fragment de code ci-dessus présente une vulnérabilité de type cross-site scripting (XSS) !

remarque

Note : Il est crucialement important de post-traiter les entrées contrôlées par l'utilisateur pour échapper à toutes les séquences de caractères spéciales, en fonction du contexte. Dans ce cas particulier, nous injectons dans une balise <script>, donc nous devons (aussi) échapper </script, <script et <!-​-.

Support pour JSON ⊂ ECMAScript